Merge branch 'dev' into master

This commit is contained in:
Jamie 2016-12-10 21:09:38 +00:00 committed by GitHub
commit a056ba17e5
206 changed files with 8047 additions and 2824 deletions

View file

@ -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";
}
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}
}

View file

@ -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 +=

View file

@ -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);

View file

@ -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

View file

@ -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; }

View file

@ -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;}

View file

@ -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;
}

View file

@ -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' });
//}]);
}());

View file

@ -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>

View file

@ -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-->

View file

@ -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>

View file

@ -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();
}
})();

View file

@ -4,10 +4,14 @@
$scope.user = {}; // The local user
$scope.users = []; // list of users
$scope.claims = []; // List of claims
$scope.features = []; // List of features
$scope.permissions = []; // List of permissions
$scope.selectedUser = {}; // User on the right side
$scope.selectedClaims = {};
$scope.selectedFeatures = {};
$scope.selectedPermissions = {};
$scope.minDate = "0001-01-01T00:00:00.0000000+00:00";
@ -15,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
@ -30,10 +31,7 @@
});
$scope.selectedUser = user[0];
if (!open) {
$("#wrapper").toggleClass("toggled");
open = true;
}
openSidebar();
}
// Get all users in the system
@ -44,60 +42,71 @@
});
};
// Get the claims and populate the create dropdown
$scope.getClaims = function () {
userManagementService.getClaims()
// Get the permissions and features and populate the create dropdown
$scope.getFeaturesPermissions = function () {
userManagementService.getFeatures()
.then(function (data) {
$scope.claims = data.data;
$scope.features = data.data;
});
userManagementService.getPermissions()
.then(function (data) {
$scope.permissions = data.data;
});
}
// 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) {
generateNotify("Please select a permission", 'warning');
return;
}
if (!$scope.selectedClaims) {
$scope.error.error = true;
$scope.error.errorMessage = "Please select a permission";
generateNotify($scope.error.errorMessage, '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;
}
}
userManagementService.addUser($scope.user, $scope.selectedClaims)
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.selectedClaims = {};
$scope.claims.forEach(function (entry) {
entry.selected = false;
});
}
clearCheckboxes();
};
});
};
$scope.hasClaim = function (claim) {
var claims = $scope.selectedUser.claimsArray;
var result = claims.some(function (item) {
return item === claim.name;
});
return result;
};
$scope.$watch('claims|filter:{selected:true}',
// Watch the checkboxes for updates (Creating a user)
$scope.$watch('features|filter:{selected:true}',
function (nv) {
$scope.selectedClaims = nv.map(function (claim) {
return claim.name;
$scope.selectedFeatures = nv.map(function (f) {
return f.name;
});
},
true);
$scope.$watch('permissions|filter:{selected:true}',
function (nv) {
$scope.selectedPermissions = nv.map(function (f) {
return f.name;
});
},
true);
@ -105,29 +114,31 @@
$scope.updateUser = function () {
var u = $scope.selectedUser;
userManagementService.updateUser(u.id, u.claimsItem, u.alias, u.emailAddress)
.then(function (data) {
if (data) {
$scope.selectedUser = data;
return successCallback("Updated User", "success");
}
});
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");
}
}, 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) {
@ -138,10 +149,19 @@
// On page load
$scope.init = function () {
$scope.getUsers();
$scope.getClaims();
$scope.getFeaturesPermissions();
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;
@ -150,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]);
angular.module('PlexRequests').controller('userManagementController', ["$scope", "userManagementService", "moment", controller]);
}());

View file

@ -4,37 +4,68 @@
$http.defaults.headers.common['Content-Type'] = 'application/json'; // Set default headers
var getUsers = function () {
return $http.get('/usermanagement/users');
var url = createBaseUrl(getBaseUrl(), '/usermanagement/users');
return $http.get(url);
};
var addUser = function (user, claims) {
if (!user || claims.length === 0) {
var addUser = function (user, permissions, features) {
if (!user || permissions.length === 0) {
return null;
}
if (!isArray(permissions)) {
permissions = [];
}
if (!isArray(features)) {
features = [];
}
var url = createBaseUrl(getBaseUrl(), '/usermanagement/createuser');
return $http({
url: '/usermanagement/createuser',
url: url,
method: "POST",
data: { username: user.username, password: user.password, claims: claims, email: user.email }
data: { username: user.username, password: user.password, permissions: permissions, features : features, email: user.email }
});
}
var getClaims = function () {
return $http.get('/usermanagement/claims');
var getFeatures = function () {
var url = createBaseUrl(getBaseUrl(), '/usermanagement/features');
return $http.get(url);
}
var updateUser = function (id, claims, alias, email) {
var getPermissions = function () {
var url = createBaseUrl(getBaseUrl(), '/usermanagement/permissions');
return $http.get(url);
}
var updateUser = function (id, permissions, features, alias, email) {
if (!isArray(permissions)) {
permissions = [];
}
if (!isArray(features)) {
features = [];
}
var url = createBaseUrl(getBaseUrl(), '/usermanagement/updateUser');
return $http({
url: '/usermanagement/updateUser',
url: url,
method: "POST",
data: { id: id, claims: claims, alias: alias, emailAddress: email }
data: { id: id, permissions: permissions, features: features, alias: alias, emailAddress: email },
});
}
var deleteUser = function (id) {
var url = createBaseUrl(getBaseUrl(), '/usermanagement/deleteUser');
return $http({
url: '/usermanagement/deleteUser',
url: url,
method: "POST",
data: { id: id }
});
@ -43,11 +74,19 @@
return {
getUsers: getUsers,
addUser: addUser,
getClaims: getClaims,
getFeatures: getFeatures,
getPermissions: getPermissions,
updateUser: updateUser,
deleteUser: deleteUser
};
}
function getBaseUrl() {
return $('#baseUrl').text();
}
function isArray(obj) {
return !!obj && Array === obj.constructor;
}
angular.module('PlexRequests').factory('userManagementService', ["$http", userManagementService]);

View file

@ -56,6 +56,17 @@ label {
margin-bottom: 0.5rem !important;
font-size: 16px !important; }
.small-label {
display: inline-block !important;
margin-bottom: 0.5rem !important;
font-size: 11px !important; }
.small-checkbox {
min-height: 0 !important; }
.round-checkbox {
border-radius: 8px; }
.nav-tabs > li {
font-size: 13px;
line-height: 21px; }
@ -257,6 +268,12 @@ label {
font-size: 15px;
padding: 3px 0; }
#cacherRunning {
background-color: #4e5d6c;
text-align: center;
font-size: 15px;
padding: 3px 0; }
.checkbox label {
display: inline-block;
cursor: pointer;
@ -288,6 +305,45 @@ label {
text-align: center;
line-height: 13px; }
.small-checkbox label {
display: inline-block;
cursor: pointer;
position: relative;
padding-left: 25px;
margin-right: 15px;
font-size: 13px;
margin-bottom: 10px; }
.small-checkbox label:before {
content: "";
display: inline-block;
width: 18px;
height: 18px;
margin-right: 10px;
position: absolute;
left: 0;
bottom: 1px;
border: 2px solid #eee;
border-radius: 8px;
min-height: 0px !important; }
.small-checkbox input[type=checkbox] {
display: none; }
.small-checkbox input[type=checkbox]:checked + label:before {
content: "\2713";
font-size: 13px;
color: #fafafa;
text-align: center;
line-height: 13px; }
.small-checkbox label {
min-height: 0 !important;
padding-left: 20px;
margin-bottom: 0;
font-weight: normal;
cursor: pointer; }
.input-group-sm {
padding-top: 2px;
padding-bottom: 2px; }
@ -363,7 +419,7 @@ label {
margin-right: -250px;
overflow-y: auto;
background: #4e5d6c;
padding-left: 15px;
padding-left: 0;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
@ -436,3 +492,20 @@ label {
position: relative;
margin-right: 0; } }
#lightbox {
background-color: grey;
filter: alpha(opacity=50);
/* IE */
opacity: 0.5;
/* Safari, Opera */
-moz-opacity: 0.50;
/* FireFox */
top: 0px;
left: 0px;
z-index: 20;
height: 100%;
width: 100%;
background-repeat: no-repeat;
background-position: center;
position: absolute; }

File diff suppressed because one or more lines are too long

View file

@ -6,9 +6,7 @@ $info-colour: #5bc0de;
$warning-colour: #f0ad4e;
$danger-colour: #d9534f;
$success-colour: #5cb85c;
$i:
!important
;
$i:!important;
@media (min-width: 768px ) {
.row {
@ -88,6 +86,21 @@ label {
margin-bottom: .5rem $i;
font-size: 16px $i;
}
.small-label {
display: inline-block $i;
margin-bottom: .5rem $i;
font-size: 11px $i;
}
.small-checkbox{
min-height:0 $i;
}
.round-checkbox {
border-radius:8px;
}
.nav-tabs > li {
font-size: 13px;
@ -329,6 +342,13 @@ $border-radius: 10px;
padding: 3px 0;
}
#cacherRunning {
background-color: $form-color;
text-align: center;
font-size: 15px;
padding: 3px 0;
}
.checkbox label {
display: inline-block;
cursor: pointer;
@ -364,6 +384,50 @@ $border-radius: 10px;
line-height: 13px;
}
.small-checkbox label {
display: inline-block;
cursor: pointer;
position: relative;
padding-left: 25px;
margin-right: 15px;
font-size: 13px;
margin-bottom: 10px;
}
.small-checkbox label:before {
content: "";
display: inline-block;
width: 18px;
height: 18px;
margin-right: 10px;
position: absolute;
left: 0;
bottom: 1px;
border: 2px solid #eee;
border-radius: 8px;
min-height:0px $i;
}
.small-checkbox input[type=checkbox] {
display: none;
}
.small-checkbox input[type=checkbox]:checked + label:before {
content: "\2713";
font-size: 13px;
color: #fafafa;
text-align: center;
line-height: 13px;
}
.small-checkbox label {
min-height: 0 $i;
padding-left: 20px;
margin-bottom: 0;
font-weight: normal;
cursor: pointer;
}
.input-group-sm {
padding-top: 2px;
padding-bottom: 2px;
@ -455,7 +519,7 @@ $border-radius: 10px;
margin-right: -250px;
overflow-y: auto;
background: #4e5d6c;
padding-left:15px;
padding-left:0;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
@ -551,4 +615,20 @@ $border-radius: 10px;
position: relative;
margin-right: 0;
}
}
}
#lightbox {
background-color: grey;
filter:alpha(opacity=50); /* IE */
opacity: 0.5; /* Safari, Opera */
-moz-opacity:0.50; /* FireFox */
top: 0px;
left: 0px;
z-index: 20;
height: 100%;
width: 100%;
background-repeat:no-repeat;
background-position:center;
position:absolute;
}

File diff suppressed because one or more lines are too long

View file

@ -16,10 +16,13 @@ var base = $('#baseUrl').text();
var tvLoaded = false;
var albumLoaded = false;
var isAdmin = $('#isAdmin').val();
var defaultFiler = isAdmin == 'True' ? '.approved-fase' : 'all';
var mixItUpDefault = {
animation: { enable: true },
load: {
filter: 'all',
filter: defaultFiler,
sort: 'requestorder:desc'
},
layout: {
@ -259,7 +262,16 @@ $('#deleteMusic').click(function (e) {
});
// filtering/sorting
$('.filter,.sort', '.dropdown-menu').click(function (e) {
$('.filter', '.dropdown-menu').click(function (e) {
var $this = $(this);
$('.fa-check-square', $this.parents('.dropdown-menu:first')).removeClass('fa-check-square').addClass('fa-square-o');
$this.children('.fa').first().removeClass('fa-square-o').addClass('fa-check-square');
$("#filterText").fadeOut(function () {
$(this).text($this.text().trim());
}).fadeIn();
});
$('.sort', '.dropdown-menu').click(function (e) {
var $this = $(this);
$('.fa-check-square', $this.parents('.dropdown-menu:first')).removeClass('fa-check-square').addClass('fa-square-o');
$this.children('.fa').first().removeClass('fa-square-o').addClass('fa-check-square');

View file

@ -51,21 +51,6 @@ $(function () {
});
focusSearch($('li.active a', '#nav-tabs').first().attr('href'));
// Get the user notification setting
var url = createBaseUrl(base, '/search/notifyuser/');
$.ajax({
type: "get",
url: url,
dataType: "json",
success: function (response) {
$('#notifyUser').prop("checked", response);
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
// Type in movie search
$("#movieSearchContent").on("input", function () {
if (searchTimer) {
@ -184,32 +169,6 @@ $(function () {
sendRequestAjax(data, type, url, buttonId);
});
// Enable/Disable user notifications
$('#saveNotificationSettings')
.click(function (e) {
e.preventDefault();
var url = createBaseUrl(base, '/search/notifyuser/');
var checked = $('#notifyUser').prop('checked');
$.ajax({
type: "post",
url: url,
data: { notify: checked },
dataType: "json",
success: function (response) {
console.log(response);
if (response.result === true) {
generateNotify(response.message || "Success!", "success");
} else {
generateNotify(response.message, "warning");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
// Report Issue
$(document).on("click", ".dropdownIssue", function (e) {
var issue = $(this).attr("issue-select");

2
PlexRequests.UI/Content/spin.min.js vendored Normal file
View file

@ -0,0 +1,2 @@
// http://spin.js.org/#v2.3.2
!function(a,b){"object"==typeof module&&module.exports?module.exports=b():"function"==typeof define&&define.amd?define(b):a.Spinner=b()}(this,function(){"use strict";function a(a,b){var c,d=document.createElement(a||"div");for(c in b)d[c]=b[c];return d}function b(a){for(var b=1,c=arguments.length;c>b;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return m[e]||(k.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",k.cssRules.length),m[e]=1),e}function d(a,b){var c,d,e=a.style;if(b=b.charAt(0).toUpperCase()+b.slice(1),void 0!==e[b])return b;for(d=0;d<l.length;d++)if(c=l[d]+b,void 0!==e[c])return c}function e(a,b){for(var c in b)a.style[d(a,c)||c]=b[c];return a}function f(a){for(var b=1;b<arguments.length;b++){var c=arguments[b];for(var d in c)void 0===a[d]&&(a[d]=c[d])}return a}function g(a,b){return"string"==typeof a?a:a[b%a.length]}function h(a){this.opts=f(a||{},h.defaults,n)}function i(){function c(b,c){return a("<"+b+' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">',c)}k.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.scale*d.width,left:d.scale*d.radius,top:-d.scale*d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.scale*(d.length+d.width),k=2*d.scale*j,l=-(d.width+d.length)*d.scale*2+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d<e.childNodes.length&&(e=e.childNodes[b+d],e=e&&e.firstChild,e=e&&e.firstChild,e&&(e.opacity=c))}}var j,k,l=["webkit","Moz","ms","O"],m={},n={lines:12,length:7,width:5,radius:10,scale:1,corners:1,color:"#000",opacity:.25,rotate:0,direction:1,speed:1,trail:100,fps:20,zIndex:2e9,className:"spinner",top:"50%",left:"50%",shadow:!1,hwaccel:!1,position:"absolute"};if(h.defaults={},f(h.prototype,{spin:function(b){this.stop();var c=this,d=c.opts,f=c.el=a(null,{className:d.className});if(e(f,{position:d.position,width:0,zIndex:d.zIndex,left:d.left,top:d.top}),b&&b.insertBefore(f,b.firstChild||null),f.setAttribute("role","progressbar"),c.lines(f,c.opts),!j){var g,h=0,i=(d.lines-1)*(1-d.direction)/2,k=d.fps,l=k/d.speed,m=(1-d.opacity)/(l*d.trail/100),n=l/d.lines;!function o(){h++;for(var a=0;a<d.lines;a++)g=Math.max(1-(h+(d.lines-a)*n)%l*m,d.opacity),c.opacity(f,a*d.direction+i,g,d);c.timeout=c.el&&setTimeout(o,~~(1e3/k))}()}return c},stop:function(){var a=this.el;return a&&(clearTimeout(this.timeout),a.parentNode&&a.parentNode.removeChild(a),this.el=void 0),this},lines:function(d,f){function h(b,c){return e(a(),{position:"absolute",width:f.scale*(f.length+f.width)+"px",height:f.scale*f.width+"px",background:b,boxShadow:c,transformOrigin:"left",transform:"rotate("+~~(360/f.lines*k+f.rotate)+"deg) translate("+f.scale*f.radius+"px,0)",borderRadius:(f.corners*f.scale*f.width>>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k<f.lines;k++)i=e(a(),{position:"absolute",top:1+~(f.scale*f.width/2)+"px",transform:f.hwaccel?"translate3d(0,0,0)":"",opacity:f.opacity,animation:j&&c(f.opacity,f.trail,l+k*f.direction,f.lines)+" "+1/f.speed+"s linear infinite"}),f.shadow&&b(i,e(h("#000","0 0 4px #000"),{top:"2px"})),b(d,b(i,h(g(f.color,k),"0 0 1px rgba(0,0,0,.1)")));return d},opacity:function(a,b,c){b<a.childNodes.length&&(a.childNodes[b].style.opacity=c)}}),"undefined"!=typeof document){k=function(){var c=a("style",{type:"text/css"});return b(document.getElementsByTagName("head")[0],c),c.sheet||c.styleSheet}();var o=e(a("group"),{behavior:"url(#default#VML)"});!d(o,"transform")&&o.adj?i():j=d(o,"animation")}return h});

View file

@ -106,31 +106,6 @@
});
$('#contentBody').on('click', '#SearchForMovies', function () {
var checked = this.checked;
changeDisabledStatus($('#RequireMovieApproval'), checked, $('#RequireMovieApprovalLabel'));
});
$('#contentBody').on('click', '#SearchForTvShows', function () {
var checked = this.checked;
changeDisabledStatus($('#RequireTvShowApproval'), checked, $('#RequireTvShowApprovalLabel'));
});
$('#contentBody').on('click', '#SearchForMusic', function () {
var checked = this.checked;
changeDisabledStatus($('#RequireMusicApproval'), checked, $('#RequireMusicApprovalLabel'));
});
function changeDisabledStatus($element, checked, $label) {
if (checked) {
$element.removeAttr("disabled");
$label.css("color","");
} else {
$element.attr("disabled","disabled");
$label.css("color", "grey");
}
}
function loadArea(templateId) {
var $body = $('#contentBody');

View file

@ -80,7 +80,7 @@ namespace PlexRequests.UI.Helpers
{
$"<link rel=\"stylesheet\" href=\"{startUrl}/bootstrap.css\" type=\"text/css\"/>",
$"<link rel=\"stylesheet\" href=\"{startUrl}/font-awesome.css\" type=\"text/css\"/>",
$"<link rel=\"stylesheet\" href=\"{startUrl}/pace.min.css\" type=\"text/css\"/>",
//$"<link rel=\"stylesheet\" href=\"{startUrl}/pace.min.css\" type=\"text/css\"/>",
$"<link rel=\"stylesheet\" href=\"{startUrl}/awesome-bootstrap-checkbox.css\" type=\"text/css\"/>",
$"<link rel=\"stylesheet\" href=\"{startUrl}/base.css?v={Assembly}\" type=\"text/css\"/>",
$"<link rel=\"stylesheet\" href=\"{startUrl}/Themes/{settings.ThemeName}?v={Assembly}\" type=\"text/css\"/>",
@ -161,6 +161,7 @@ namespace PlexRequests.UI.Helpers
var content = GetContentUrl(assetLocation);
sb.AppendLine($"<script src=\"{content}/Content/clipboard.min.js\" type=\"text/javascript\"></script>");
sb.AppendLine($"<script src=\"{content}/Content/bootstrap-switch.min.js\" type=\"text/javascript\"></script>");
sb.AppendLine($"<link rel=\"stylesheet\" href=\"{content}/Content/bootstrap-switch.min.css\" type=\"text/css\"/>");
@ -223,8 +224,11 @@ namespace PlexRequests.UI.Helpers
sb.Append($"<script src=\"{content}/Content/app/userManagement/userManagementController.js?v={Assembly}\" type=\"text/javascript\"></script>");
sb.Append($"<script src=\"{content}/Content/app/userManagement/userManagementService.js?v={Assembly}\" type=\"text/javascript\"></script>");
sb.Append($"<script src=\"{content}/Content/app/userManagement/Directives/userManagementDirective.js?v={Assembly}\" type=\"text/javascript\"></script>");
sb.Append($"<script src=\"{content}/Content/moment.min.js\"></script>");
sb.Append($"<script src=\"{content}/Content/spin.min.js\"></script>");
sb.Append($"<script src=\"{content}/Content/Angular/angular-spinner.min.js\"></script>");
sb.Append($"<script src=\"{content}/Content/Angular/angular-loading-spinner.js\"></script>");
return helper.Raw(sb.ToString());
}
@ -259,6 +263,23 @@ namespace PlexRequests.UI.Helpers
return helper.Raw(asset);
}
public static IHtmlString LoadFavIcon(this HtmlHelpers helper)
{
var settings = GetSettings();
if (!settings.CollectAnalyticData)
{
return helper.Raw(string.Empty);
}
var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation);
var asset = $"<link rel=\"SHORTCUT ICON\" href=\"{content}/Content/favicon.ico\" />";
asset += $"<link rel=\"icon\" href=\"{content}/Content/favicon.ico\" type=\"image/ico\" />";
return helper.Raw(asset);
}
public static IHtmlString GetSidebarUrl(this HtmlHelpers helper, NancyContext context, string url, string title)
{
var content = GetLinkUrl(GetBaseUrl());

View file

@ -24,10 +24,9 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Reflection;
using System.Text;
using Nancy.ViewEngines.Razor;
using PlexRequests.Helpers;
namespace PlexRequests.UI.Helpers
@ -41,5 +40,17 @@ namespace PlexRequests.UI.Helpers
return helper.Raw(htmlString);
}
public static IHtmlString Checkbox(this HtmlHelpers helper, bool check, string name, string display)
{
var sb = new StringBuilder();
sb.AppendLine("<div class=\"form-group\">");
sb.AppendLine("<div class=\"checkbox\">");
sb.AppendFormat("<input type=\"checkbox\" id=\"{0}\" name=\"{0}\" {2}><label for=\"{0}\">{1}</label>", name, display, check ? "checked=\"checked\"" : string.Empty);
sb.AppendLine("</div>");
sb.AppendLine("</div>");
return helper.Raw(sb.ToString());
}
}
}

View file

@ -1,176 +0,0 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: HeadphonesSender.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.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
{
public class HeadphonesSender
{
public HeadphonesSender(IHeadphonesApi api, HeadphonesSettings settings, IRequestService request)
{
Api = api;
Settings = settings;
RequestService = request;
}
private int WaitTime => 2000;
private int CounterMax => 60;
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private IHeadphonesApi Api { get; }
private IRequestService RequestService { get; }
private HeadphonesSettings Settings { get; }
public async Task<bool> AddAlbum(RequestedModel request)
{
var addArtistResult = await AddArtist(request);
if (!addArtistResult)
{
return false;
}
// Artist is now active
// Add album
var albumResult = await Api.AddAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId);
if (!albumResult)
{
Log.Error("Couldn't add the album to headphones");
}
// Set the status to wanted and search
var status = await SetAlbumStatus(request);
if (!status)
{
return false;
}
// Approve it
request.Approved = true;
// Update the record
var updated = RequestService.UpdateRequest(request);
return updated;
}
private async Task<bool> AddArtist(RequestedModel request)
{
var index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri);
var artistExists = index.Any(x => x.ArtistID == request.ArtistId);
if (!artistExists)
{
var artistAdd = Api.AddArtist(Settings.ApiKey, Settings.FullUri, request.ArtistId);
Log.Info("Artist add result : {0}", artistAdd);
}
var counter = 0;
while (index.All(x => x.ArtistID != request.ArtistId))
{
Thread.Sleep(WaitTime);
counter++;
Log.Trace("Artist is still not present in the index. Counter = {0}", counter);
index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri);
if (counter > CounterMax)
{
Log.Trace("Artist is still not present in the index. Counter = {0}. Returning false", counter);
Log.Warn("We have tried adding the artist but it seems they are still not in headphones.");
return false;
}
}
counter = 0;
var artistStatus = index.Where(x => x.ArtistID == request.ArtistId).Select(x => x.Status).FirstOrDefault();
while (artistStatus != "Active")
{
Thread.Sleep(WaitTime);
counter++;
Log.Trace("Artist status {1}. Counter = {0}", counter, artistStatus);
index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri);
artistStatus = index.Where(x => x.ArtistID == request.ArtistId).Select(x => x.Status).FirstOrDefault();
if (counter > CounterMax)
{
Log.Trace("Artist status is still not active. Counter = {0}. Returning false", counter);
Log.Warn("The artist status is still not Active. We have waited long enough, seems to be a big delay in headphones.");
return false;
}
}
var addedArtist = index.FirstOrDefault(x => x.ArtistID == request.ArtistId);
var artistName = addedArtist?.ArtistName ?? string.Empty;
counter = 0;
while (artistName.Contains("Fetch failed"))
{
Thread.Sleep(WaitTime);
await Api.RefreshArtist(Settings.ApiKey, Settings.FullUri, request.ArtistId);
index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri);
artistName = index?.FirstOrDefault(x => x.ArtistID == request.ArtistId)?.ArtistName ?? string.Empty;
counter++;
if (counter > CounterMax)
{
Log.Trace("Artist fetch has failed. Counter = {0}. Returning false", counter);
Log.Warn("Artist in headphones fetch has failed, we have tried refreshing the artist but no luck.");
return false;
}
}
return true;
}
private async Task<bool> SetAlbumStatus(RequestedModel request)
{
var counter = 0;
var setStatus = await Api.QueueAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId);
while (!setStatus)
{
Thread.Sleep(WaitTime);
counter++;
Log.Trace("Setting Album status. Counter = {0}", counter);
setStatus = await Api.QueueAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId);
if (counter > CounterMax)
{
Log.Trace("Album status is still not active. Counter = {0}. Returning false", counter);
Log.Warn("We tried to se the status for the album but headphones didn't want to snatch it.");
return false;
}
}
return true;
}
}
}

View file

@ -25,25 +25,60 @@
// ************************************************************************/
#endregion
using Nancy;
using Nancy.Security;
using Nancy.ViewEngines.Razor;
using PlexRequests.Helpers.Permissions;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Helpers
{
public static class HtmlSecurityHelper
{
public static bool HasAnyPermission(this HtmlHelpers helper, params string[] claims)
private static ISecurityExtensions Security
{
if (!helper.CurrentUser.IsAuthenticated())
get
{
return false;
var security = ServiceLocator.Instance.Resolve<ISecurityExtensions>();
return _security ?? (_security = security);
}
return helper.CurrentUser.HasAnyClaim(claims);
}
public static bool DoesNotHaveAnyPermission(this HtmlHelpers helper, params string[] claims)
private static ISecurityExtensions _security;
public static bool HasAnyPermission(this HtmlHelpers helper, bool authenticated = true, params Permissions[] permission)
{
return SecurityExtensions.DoesNotHaveClaims(claims, helper.CurrentUser);
if (authenticated)
{
return helper.CurrentUser.IsAuthenticated()
&& Security.HasAnyPermissions(helper.CurrentUser, permission);
}
return Security.HasAnyPermissions(helper.CurrentUser, permission);
}
public static bool DoesNotHavePermission(this HtmlHelpers helper, int permission)
{
return Security.DoesNotHavePermissions(permission, helper.CurrentUser);
}
public static bool IsAdmin(this HtmlHelpers helper, bool isAuthenticated = true)
{
return HasAnyPermission(helper, isAuthenticated, Permissions.Administrator);
}
public static bool IsLoggedIn(this HtmlHelpers helper, NancyContext context)
{
return Security.IsLoggedIn(context);
}
public static bool IsPlexUser(this HtmlHelpers helper)
{
return Security.IsPlexUser(helper.CurrentUser);
}
public static bool IsNormalUser(this HtmlHelpers helper)
{
return Security.IsNormalUser(helper.CurrentUser);
}
}
}

View file

@ -1,170 +0,0 @@
#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 System.Collections.Generic;
using System.Linq;
using Nancy;
using Nancy.Extensions;
using Nancy.Security;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Helpers
{
public static class SecurityExtensions
{
public static bool IsLoggedIn(this 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 static bool IsPlexUser(this NancyContext context)
{
var userName = context.Request.Session[SessionKeys.UsernameKey];
var plexUser = userName != null;
var isAuth = context.CurrentUser?.IsAuthenticated() ?? false;
return plexUser && !isAuth;
}
public static bool IsNormalUser(this NancyContext context)
{
var userName = context.Request.Session[SessionKeys.UsernameKey];
var plexUser = userName != null;
var isAuth = context.CurrentUser?.IsAuthenticated() ?? false;
return isAuth && !plexUser;
}
/// <summary>
/// This module requires authentication and NO certain claims to be present.
/// </summary>
/// <param name="module">Module to enable</param>
/// <param name="requiredClaims">Claim(s) required</param>
public static void DoesNotHaveClaim(this INancyModule module, params string[] bannedClaims)
{
module.AddBeforeHookOrExecute(SecurityHooks.RequiresAuthentication(), "Requires Authentication");
module.AddBeforeHookOrExecute(DoesNotHaveClaims(bannedClaims), "Has Banned Claims");
}
public static bool DoesNotHaveClaimCheck(this INancyModule module, params string[] bannedClaims)
{
if (!module.Context?.CurrentUser?.IsAuthenticated() ?? false)
{
return false;
}
if (DoesNotHaveClaims(bannedClaims, module.Context))
{
return false;
}
return true;
}
public static bool DoesNotHaveClaimCheck(this NancyContext context, params string[] bannedClaims)
{
if (!context?.CurrentUser?.IsAuthenticated() ?? false)
{
return false;
}
if (DoesNotHaveClaims(bannedClaims, context))
{
return false;
}
return true;
}
/// <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="claims">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 static Func<NancyContext, Response> DoesNotHaveClaims(IEnumerable<string> claims)
{
return ForbiddenIfNot(ctx => !ctx.CurrentUser.HasAnyClaim(claims));
}
public static bool DoesNotHaveClaims(IEnumerable<string> claims, NancyContext ctx)
{
return !ctx.CurrentUser.HasAnyClaim(claims);
}
public static bool DoesNotHaveClaims(IEnumerable<string> claims, IUserIdentity identity)
{
return !identity?.HasAnyClaim(claims) ?? true;
}
// 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>
private static 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>
private static Func<NancyContext, Response> HttpStatusCodeIfNot(HttpStatusCode statusCode, Func<NancyContext, bool> test)
{
return ctx =>
{
Response response = null;
if (!test(ctx))
response = new Response
{
StatusCode = statusCode
};
return response;
};
}
}
}

View file

@ -1,375 +0,0 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: TvSender.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 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
{
public class TvSender
{
public TvSender(ISonarrApi sonarrApi, ISickRageApi srApi)
{
SonarrApi = sonarrApi;
SickrageApi = srApi;
}
private ISonarrApi SonarrApi { get; }
private ISickRageApi SickrageApi { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public async Task<SonarrAddSeries> SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model)
{
return await SendToSonarr(sonarrSettings, model, string.Empty);
}
/// <summary>
/// Broken Way
/// </summary>
/// <param name="sonarrSettings"></param>
/// <param name="model"></param>
/// <param name="qualityId"></param>
/// <returns></returns>
public async Task<SonarrAddSeries> SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model, string qualityId)
{
var qualityProfile = 0;
var episodeRequest = model.Episodes.Any();
if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality
{
int.TryParse(qualityId, out qualityProfile);
}
if (qualityProfile <= 0)
{
int.TryParse(sonarrSettings.QualityProfile, out qualityProfile);
}
var series = await GetSonarrSeries(sonarrSettings, model.ProviderId);
var requestAll = model.SeasonsRequested?.Equals("All", StringComparison.CurrentCultureIgnoreCase);
var first = model.SeasonsRequested?.Equals("First", StringComparison.CurrentCultureIgnoreCase);
var latest = model.SeasonsRequested?.Equals("Latest", StringComparison.CurrentCultureIgnoreCase);
var specificSeasonRequest = model.SeasonList?.Any();
if (episodeRequest)
{
// Does series exist?
if (series != null)
{
// Series Exists
// Request the episodes in the existing series
await RequestEpisodesWithExistingSeries(model, series, sonarrSettings);
return new SonarrAddSeries { title = series.title };
}
// Series doesn't exist, need to add it as unmonitored.
var addResult = await Task.Run(() => SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, new int[0], sonarrSettings.ApiKey,
sonarrSettings.FullUri, false));
// Get the series that was just added
series = await GetSonarrSeries(sonarrSettings, model.ProviderId);
series.monitored = true; // We want to make sure we are monitoring the series
// Un-monitor all seasons
foreach (var season in series.seasons)
{
season.monitored = false;
}
// Update the series, Since we cannot add as un-monitored due to the following bug: https://github.com/Sonarr/Sonarr/issues/1404
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
// We now have the series in Sonarr, update it to request the episodes.
await RequestAllEpisodesInASeasonWithExistingSeries(model, series, sonarrSettings);
return addResult;
}
// Series exists, don't need to add it
if (series == null)
{
// Set the series as monitored with a season count as 0 so it doesn't search for anything
SonarrApi.AddSeriesNew(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, new int[] {1,2,3,4,5,6,7,8,9,10,11,12,13}, sonarrSettings.ApiKey,
sonarrSettings.FullUri);
await Task.Delay(TimeSpan.FromSeconds(1));
series = await GetSonarrSeries(sonarrSettings, model.ProviderId);
foreach (var s in series.seasons)
{
s.monitored = false;
}
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
if (requestAll ?? false)
{
// Monitor all seasons
foreach (var season in series.seasons)
{
season.monitored = true;
}
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
SonarrApi.SearchForSeries(series.id, sonarrSettings.ApiKey, sonarrSettings.FullUri); // Search For all episodes!"
//// This is a work around for this issue: https://github.com/Sonarr/Sonarr/issues/1507
//// The above is the previous code.
//SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
// sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, model.SeasonList, sonarrSettings.ApiKey,
// sonarrSettings.FullUri, true, true);
return new SonarrAddSeries { title = series.title }; // We have updated it
}
if (first ?? false)
{
var firstSeries = (series?.seasons?.OrderBy(x => x.seasonNumber)).FirstOrDefault(x => x.seasonNumber > 0) ?? new Season();
firstSeries.monitored = true;
var episodes = SonarrApi.GetEpisodes(series.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri); // Need to get the episodes so we mark them as monitored
var episodesToUpdate = new List<SonarrEpisodes>();
foreach (var e in episodes)
{
if (e.hasFile || e.seasonNumber != firstSeries.seasonNumber)
{
continue;
}
e.monitored = true; // Mark only the episodes we want as monitored
episodesToUpdate.Add(e);
}
foreach (var sonarrEpisode in episodesToUpdate)
{
SonarrApi.UpdateEpisode(sonarrEpisode, sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
SonarrApi.SearchForSeason(series.id, firstSeries.seasonNumber, sonarrSettings.ApiKey,
sonarrSettings.FullUri);
return new SonarrAddSeries { title = series.title }; // We have updated it
}
if (latest ?? false)
{
var lastSeries = series?.seasons?.OrderByDescending(x => x.seasonNumber)?.FirstOrDefault() ?? new Season();
lastSeries.monitored = true;
var episodes = SonarrApi.GetEpisodes(series.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri); // Need to get the episodes so we mark them as monitored
var episodesToUpdate = new List<SonarrEpisodes>();
foreach (var e in episodes)
{
if (e.hasFile || e.seasonNumber != lastSeries.seasonNumber)
{
continue;
}
e.monitored = true; // Mark only the episodes we want as monitored
episodesToUpdate.Add(e);
}
foreach (var sonarrEpisode in episodesToUpdate)
{
SonarrApi.UpdateEpisode(sonarrEpisode, sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
SonarrApi.SearchForSeason(series.id, lastSeries.seasonNumber, sonarrSettings.ApiKey,
sonarrSettings.FullUri);
return new SonarrAddSeries { title = series.title }; // We have updated it
}
if (specificSeasonRequest ?? false)
{
// Monitor the seasons that we have chosen
foreach (var season in series.seasons)
{
if (model.SeasonList.Contains(season.seasonNumber))
{
season.monitored = true;
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
SonarrApi.SearchForSeason(series.id, season.seasonNumber, sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
}
return new SonarrAddSeries { title = series.title }; // We have updated it
}
return null;
}
public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model)
{
return SendToSickRage(sickRageSettings, model, sickRageSettings.QualityProfile);
}
public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model, string qualityId)
{
Log.Info("Sending to SickRage {0}", model.Title);
if (sickRageSettings.Qualities.All(x => x.Key != qualityId))
{
qualityId = sickRageSettings.QualityProfile;
}
var apiResult = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, qualityId,
sickRageSettings.ApiKey, sickRageSettings.FullUri);
var result = apiResult.Result;
return result;
}
internal async Task RequestEpisodesWithExistingSeries(RequestedModel model, Series selectedSeries, SonarrSettings sonarrSettings)
{
// Show Exists
// Look up all episodes
var ep = SonarrApi.GetEpisodes(selectedSeries.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
var episodes = ep?.ToList() ?? new List<SonarrEpisodes>();
var internalEpisodeIds = new List<int>();
var tasks = new List<Task>();
foreach (var r in model.Episodes)
{
// Match the episode and season number.
// If the episode is monitored we might not be searching for it.
var episode =
episodes.FirstOrDefault(
x => x.episodeNumber == r.EpisodeNumber && x.seasonNumber == r.SeasonNumber);
if (episode == null)
{
continue;
}
var episodeInfo = SonarrApi.GetEpisode(episode.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
episodeInfo.monitored = true; // Set the episode to monitored
tasks.Add(Task.Run(() => SonarrApi.UpdateEpisode(episodeInfo, sonarrSettings.ApiKey,
sonarrSettings.FullUri)));
internalEpisodeIds.Add(episode.id);
}
await Task.WhenAll(tasks.ToArray());
SonarrApi.SearchForEpisodes(internalEpisodeIds.ToArray(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
internal async Task RequestAllEpisodesWithExistingSeries(RequestedModel model, Series selectedSeries, SonarrSettings sonarrSettings)
{
// Show Exists
// Look up all episodes
var ep = SonarrApi.GetEpisodes(selectedSeries.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
var episodes = ep?.ToList() ?? new List<SonarrEpisodes>();
var internalEpisodeIds = new List<int>();
var tasks = new List<Task>();
foreach (var r in episodes)
{
if (r.monitored || r.hasFile) // If it's already monitored or has the file, there is no point in updating it
{
continue;
}
// Lookup the individual episode details
var episodeInfo = SonarrApi.GetEpisode(r.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
episodeInfo.monitored = true; // Set the episode to monitored
tasks.Add(Task.Run(() => SonarrApi.UpdateEpisode(episodeInfo, sonarrSettings.ApiKey,
sonarrSettings.FullUri)));
internalEpisodeIds.Add(r.id);
}
await Task.WhenAll(tasks.ToArray());
SonarrApi.SearchForEpisodes(internalEpisodeIds.ToArray(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
internal async Task RequestAllEpisodesInASeasonWithExistingSeries(RequestedModel model, Series selectedSeries, SonarrSettings sonarrSettings)
{
// Show Exists
// Look up all episodes
var ep = SonarrApi.GetEpisodes(selectedSeries.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
var episodes = ep?.ToList() ?? new List<SonarrEpisodes>();
var internalEpisodeIds = new List<int>();
var tasks = new List<Task>();
var requestedEpisodes = model.Episodes;
foreach (var r in episodes)
{
if (r.hasFile) // If it already has the file, there is no point in updating it
{
continue;
}
var epComparison = new EpisodesModel
{
EpisodeNumber = r.episodeNumber,
SeasonNumber = r.seasonNumber
};
// Make sure we are looking for the right episode and season
if (!requestedEpisodes.Contains(epComparison))
{
continue;
}
// Lookup the individual episode details
var episodeInfo = SonarrApi.GetEpisode(r.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
// If the season is not in thr
episodeInfo.monitored = true; // Set the episode to monitored
tasks.Add(Task.Run(() => SonarrApi.UpdateEpisode(episodeInfo, sonarrSettings.ApiKey,
sonarrSettings.FullUri)));
internalEpisodeIds.Add(r.id);
}
await Task.WhenAll(tasks.ToArray());
SonarrApi.SearchForEpisodes(internalEpisodeIds.ToArray(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
private async Task<Series> GetSonarrSeries(SonarrSettings sonarrSettings, int showId)
{
var task = await Task.Run(() => SonarrApi.GetSeries(sonarrSettings.ApiKey, sonarrSettings.FullUri)).ConfigureAwait(false);
var selectedSeries = task.FirstOrDefault(series => series.tvdbId == showId);
return selectedSeries;
}
}
}

View file

@ -1,302 +0,0 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: TvSenderOld.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.SickRage;
using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
namespace PlexRequests.UI.Helpers
{
public class TvSenderOld
{
public TvSenderOld(ISonarrApi sonarrApi, ISickRageApi srApi)
{
SonarrApi = sonarrApi;
SickrageApi = srApi;
}
private ISonarrApi SonarrApi { get; }
private ISickRageApi SickrageApi { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public async Task<SonarrAddSeries> SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model)
{
return await SendToSonarr(sonarrSettings, model, string.Empty);
}
public async Task<SonarrAddSeries> SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model, string qualityId)
{
var qualityProfile = 0;
var episodeRequest = model.Episodes.Any();
if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality
{
int.TryParse(qualityId, out qualityProfile);
}
if (qualityProfile <= 0)
{
int.TryParse(sonarrSettings.QualityProfile, out qualityProfile);
}
var series = await GetSonarrSeries(sonarrSettings, model.ProviderId);
if (episodeRequest)
{
// Does series exist?
if (series != null)
{
// Series Exists
// Request the episodes in the existing series
await RequestEpisodesWithExistingSeries(model, series, sonarrSettings);
return new SonarrAddSeries { title = series.title };
}
// Series doesn't exist, need to add it as unmonitored.
var addResult = await Task.Run(() => SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, new int[0], sonarrSettings.ApiKey,
sonarrSettings.FullUri, false));
// Get the series that was just added
series = await GetSonarrSeries(sonarrSettings, model.ProviderId);
series.monitored = true; // We want to make sure we are monitoring the series
// Un-monitor all seasons
foreach (var season in series.seasons)
{
season.monitored = false;
}
// Update the series, Since we cannot add as un-monitored due to the following bug: https://github.com/Sonarr/Sonarr/issues/1404
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
// We now have the series in Sonarr, update it to request the episodes.
await RequestAllEpisodesInASeasonWithExistingSeries(model, series, sonarrSettings);
return addResult;
}
if (series != null)
{
var requestAll = model.SeasonsRequested.Equals("All", StringComparison.CurrentCultureIgnoreCase);
var first = model.SeasonsRequested.Equals("First", StringComparison.CurrentCultureIgnoreCase);
var latest = model.SeasonsRequested.Equals("Latest", StringComparison.CurrentCultureIgnoreCase);
if (model.SeasonList.Any())
{
// Monitor the seasons that we have chosen
foreach (var season in series.seasons)
{
if (model.SeasonList.Contains(season.seasonNumber))
{
season.monitored = true;
}
}
}
if (requestAll)
{
// Monitor all seasons
foreach (var season in series.seasons)
{
season.monitored = true;
}
}
if (first)
{
var firstSeries = series?.seasons?.OrderBy(x => x.seasonNumber)?.FirstOrDefault() ?? new Season();
firstSeries.monitored = true;
}
if (latest)
{
var lastSeries = series?.seasons?.OrderByDescending(x => x.seasonNumber)?.FirstOrDefault() ?? new Season();
lastSeries.monitored = true;
}
// Update the series in sonarr with the new monitored status
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
await RequestAllEpisodesInASeasonWithExistingSeries(model, series, sonarrSettings);
return new SonarrAddSeries { title = series.title }; // We have updated it
}
var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey,
sonarrSettings.FullUri, true, true);
return result;
}
public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model)
{
return SendToSickRage(sickRageSettings, model, sickRageSettings.QualityProfile);
}
public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model, string qualityId)
{
Log.Info("Sending to SickRage {0}", model.Title);
if (sickRageSettings.Qualities.All(x => x.Key != qualityId))
{
qualityId = sickRageSettings.QualityProfile;
}
var apiResult = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, qualityId,
sickRageSettings.ApiKey, sickRageSettings.FullUri);
var result = apiResult.Result;
return result;
}
internal async Task RequestEpisodesWithExistingSeries(RequestedModel model, Series selectedSeries, SonarrSettings sonarrSettings)
{
// Show Exists
// Look up all episodes
var ep = SonarrApi.GetEpisodes(selectedSeries.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
var episodes = ep?.ToList() ?? new List<SonarrEpisodes>();
var internalEpisodeIds = new List<int>();
var tasks = new List<Task>();
foreach (var r in model.Episodes)
{
// Match the episode and season number.
// If the episode is monitored we might not be searching for it.
var episode =
episodes.FirstOrDefault(
x => x.episodeNumber == r.EpisodeNumber && x.seasonNumber == r.SeasonNumber);
if (episode == null)
{
continue;
}
var episodeInfo = SonarrApi.GetEpisode(episode.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
episodeInfo.monitored = true; // Set the episode to monitored
tasks.Add(Task.Run(() => SonarrApi.UpdateEpisode(episodeInfo, sonarrSettings.ApiKey,
sonarrSettings.FullUri)));
internalEpisodeIds.Add(episode.id);
}
await Task.WhenAll(tasks.ToArray());
SonarrApi.SearchForEpisodes(internalEpisodeIds.ToArray(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
internal async Task RequestAllEpisodesWithExistingSeries(RequestedModel model, Series selectedSeries, SonarrSettings sonarrSettings)
{
// Show Exists
// Look up all episodes
var ep = SonarrApi.GetEpisodes(selectedSeries.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
var episodes = ep?.ToList() ?? new List<SonarrEpisodes>();
var internalEpisodeIds = new List<int>();
var tasks = new List<Task>();
foreach (var r in episodes)
{
if (r.monitored || r.hasFile) // If it's already montiored or has the file, there is no point in updating it
{
continue;
}
// Lookup the individual episode details
var episodeInfo = SonarrApi.GetEpisode(r.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
episodeInfo.monitored = true; // Set the episode to monitored
tasks.Add(Task.Run(() => SonarrApi.UpdateEpisode(episodeInfo, sonarrSettings.ApiKey,
sonarrSettings.FullUri)));
internalEpisodeIds.Add(r.id);
}
await Task.WhenAll(tasks.ToArray());
SonarrApi.SearchForEpisodes(internalEpisodeIds.ToArray(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
internal async Task RequestAllEpisodesInASeasonWithExistingSeries(RequestedModel model, Series selectedSeries, SonarrSettings sonarrSettings)
{
// Show Exists
// Look up all episodes
var ep = SonarrApi.GetEpisodes(selectedSeries.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
var episodes = ep?.ToList() ?? new List<SonarrEpisodes>();
var internalEpisodeIds = new List<int>();
var tasks = new List<Task>();
var requestedEpisodes = model.Episodes;
foreach (var r in episodes)
{
if (r.hasFile) // If it already has the file, there is no point in updating it
{
continue;
}
var epComparison = new EpisodesModel
{
EpisodeNumber = r.episodeNumber,
SeasonNumber = r.seasonNumber
};
// Make sure we are looking for the right episode and season
if (!requestedEpisodes.Contains(epComparison))
{
continue;
}
// Lookup the individual episode details
var episodeInfo = SonarrApi.GetEpisode(r.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
// If the season is not in thr
episodeInfo.monitored = true; // Set the episode to monitored
tasks.Add(Task.Run(() => SonarrApi.UpdateEpisode(episodeInfo, sonarrSettings.ApiKey,
sonarrSettings.FullUri)));
internalEpisodeIds.Add(r.id);
}
await Task.WhenAll(tasks.ToArray());
SonarrApi.SearchForEpisodes(internalEpisodeIds.ToArray(), sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
private async Task<Series> GetSonarrSeries(SonarrSettings sonarrSettings, int showId)
{
var task = await Task.Run(() => SonarrApi.GetSeries(sonarrSettings.ApiKey, sonarrSettings.FullUri)).ConfigureAwait(false);
var selectedSeries = task.FirstOrDefault(series => series.tvdbId == showId);
return selectedSeries;
}
}
}

View file

@ -62,18 +62,19 @@ namespace PlexRequests.UI.Jobs
var jobList = new List<IJobDetail>
{
JobBuilder.Create<PlexAvailabilityChecker>().WithIdentity("PlexAvailabilityChecker", "Plex").Build(),
JobBuilder.Create<PlexEpisodeCacher>().WithIdentity("PlexEpisodeCacher", "Cache").Build(),
JobBuilder.Create<PlexContentCacher>().WithIdentity("PlexContentCacher", "PlexCacher").Build(),
JobBuilder.Create<PlexEpisodeCacher>().WithIdentity("PlexEpisodeCacher", "Plex").Build(),
JobBuilder.Create<PlexUserChecker>().WithIdentity("PlexUserChecker", "Plex").Build(),
JobBuilder.Create<SickRageCacher>().WithIdentity("SickRageCacher", "Cache").Build(),
JobBuilder.Create<SonarrCacher>().WithIdentity("SonarrCacher", "Cache").Build(),
JobBuilder.Create<CouchPotatoCacher>().WithIdentity("CouchPotatoCacher", "Cache").Build(),
JobBuilder.Create<StoreBackup>().WithIdentity("StoreBackup", "Database").Build(),
JobBuilder.Create<StoreCleanup>().WithIdentity("StoreCleanup", "Database").Build(),
JobBuilder.Create<UserRequestLimitResetter>().WithIdentity("UserRequestLimiter", "Request").Build(),
JobBuilder.Create<RecentlyAdded>().WithIdentity("RecentlyAddedModel", "Email").Build()
JobBuilder.Create<RecentlyAdded>().WithIdentity("RecentlyAddedModel", "Email").Build(),
JobBuilder.Create<FaultQueueHandler>().WithIdentity("FaultQueueHandler", "Fault").Build(),
};
jobs.AddRange(jobList);
return jobs;
@ -92,8 +93,8 @@ namespace PlexRequests.UI.Jobs
var jobs = CreateJobs();
var triggers = CreateTriggers();
var jobDetails = jobs as IJobDetail[] ?? jobs.ToArray();
var triggerDetails = triggers as ITrigger[] ?? triggers.ToArray();
var jobDetails = jobs as IJobDetail[] ?? jobs.OrderByDescending(x => x.Key.Name).ToArray();
var triggerDetails = triggers as ITrigger[] ?? triggers.OrderByDescending(x => x.Key.Name).ToArray();
if (jobDetails.Length != triggerDetails.Length)
{
@ -151,56 +152,80 @@ namespace PlexRequests.UI.Jobs
{
s.UserRequestLimitResetter = 12;
}
if (s.FaultQueueHandler == 0)
{
s.FaultQueueHandler = 6;
}
if (s.PlexContentCacher == 0)
{
s.PlexContentCacher = 60;
}
if (s.PlexUserChecker == 0)
{
s.PlexUserChecker = 24;
}
var triggers = new List<ITrigger>();
var plexAvailabilityChecker =
TriggerBuilder.Create()
.WithIdentity("PlexAvailabilityChecker", "Plex")
.StartNow()
.StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexAvailabilityChecker).RepeatForever())
.Build();
var plexCacher =
TriggerBuilder.Create()
.WithIdentity("PlexContentCacher", "PlexCacher")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexContentCacher).RepeatForever())
.Build();
var plexUserChecker =
TriggerBuilder.Create()
.WithIdentity("PlexUserChecker", "Plex")
.StartAt(DateBuilder.FutureDate(30, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexUserChecker).RepeatForever())
.Build();
var srCacher =
TriggerBuilder.Create()
.WithIdentity("SickRageCacher", "Cache")
.StartNow()
.StartAt(DateBuilder.FutureDate(2, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SickRageCacher).RepeatForever())
.Build();
var sonarrCacher =
TriggerBuilder.Create()
.WithIdentity("SonarrCacher", "Cache")
.StartNow()
.StartAt(DateBuilder.FutureDate(3, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SonarrCacher).RepeatForever())
.Build();
var cpCacher =
TriggerBuilder.Create()
.WithIdentity("CouchPotatoCacher", "Cache")
.StartNow()
.StartAt(DateBuilder.FutureDate(4, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.CouchPotatoCacher).RepeatForever())
.Build();
var storeBackup =
TriggerBuilder.Create()
.WithIdentity("StoreBackup", "Database")
.StartNow()
.StartAt(DateBuilder.FutureDate(20, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreBackup).RepeatForever())
.Build();
var storeCleanup =
TriggerBuilder.Create()
.WithIdentity("StoreCleanup", "Database")
.StartNow()
.StartAt(DateBuilder.FutureDate(35, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreCleanup).RepeatForever())
.Build();
var userRequestLimiter =
TriggerBuilder.Create()
.WithIdentity("UserRequestLimiter", "Request")
.StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute))
.StartAt(DateBuilder.FutureDate(25, IntervalUnit.Minute))
// Everything has started on application start, lets wait 5 minutes
.WithSimpleSchedule(x => x.WithIntervalInHours(s.UserRequestLimitResetter).RepeatForever())
.Build();
@ -208,7 +233,7 @@ namespace PlexRequests.UI.Jobs
var plexEpCacher =
TriggerBuilder.Create()
.WithIdentity("PlexEpisodeCacher", "Cache")
.StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute))
.StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInHours(s.PlexEpisodeCacher).RepeatForever())
.Build();
@ -218,7 +243,14 @@ namespace PlexRequests.UI.Jobs
.WithIdentity("RecentlyAddedModel", "Email")
.StartNow()
.WithCronSchedule(s.RecentlyAddedCron)
.WithSimpleSchedule(x => x.WithIntervalInHours(2).RepeatForever())
.Build();
var fault =
TriggerBuilder.Create()
.WithIdentity("FaultQueueHandler", "Fault")
//.StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute))
.StartAt(DateBuilder.FutureDate(13, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInHours(s.FaultQueueHandler).RepeatForever())
.Build();
triggers.Add(rencentlyAdded);
@ -230,6 +262,9 @@ namespace PlexRequests.UI.Jobs
triggers.Add(storeCleanup);
triggers.Add(userRequestLimiter);
triggers.Add(plexEpCacher);
triggers.Add(fault);
triggers.Add(plexCacher);
triggers.Add(plexUserChecker);
return triggers;
}

View file

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SessionKeys.cs
// File: FaultedRequestsViewModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -24,13 +24,34 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using PlexRequests.Store;
using PlexRequests.Store.Models;
namespace PlexRequests.UI.Models
{
public class SessionKeys
public class FaultedRequestsViewModel
{
public const string UsernameKey = "Username";
public const string ClientDateTimeOffsetKey = "ClientDateTimeOffset";
public const string UserWizardPlexAuth = nameof(UserWizardPlexAuth);
public const string UserWizardMachineId = nameof(UserWizardMachineId);
public int Id { get; set; }
public string PrimaryIdentifier { get; set; }
public RequestTypeViewModel Type { get; set; }
public string Title { get; set; }
public FaultTypeViewModel FaultType { get; set; }
public DateTime? LastRetry { get; set; }
public string Message { get; set; }
}
}
public enum RequestTypeViewModel
{
Movie,
TvShow,
Album
}
public enum FaultTypeViewModel
{
RequestFault,
MissingInformation
}
}

View file

@ -10,17 +10,20 @@ namespace PlexRequests.UI.Models
public UserManagementUsersViewModel()
{
PlexInfo = new UserManagementPlexInformation();
Permissions = new List<CheckBox>();
Features = new List<CheckBox>();
}
public string Username { get; set; }
public string Claims { get; set; }
public string FeaturesFormattedString { get; set; }
public string PermissionsFormattedString { get; set; }
public string Id { get; set; }
public string Alias { get; set; }
public UserType Type { get; set; }
public string EmailAddress { get; set; }
public UserManagementPlexInformation PlexInfo { get; set; }
public string[] ClaimsArray { get; set; }
public List<UserManagementUpdateModel.ClaimsModel> ClaimsItem { get; set; }
public DateTime LastLoggedIn { get; set; }
public List<CheckBox> Permissions { get; set; }
public List<CheckBox> Features { get; set; }
}
public class UserManagementPlexInformation
@ -33,6 +36,13 @@ namespace PlexRequests.UI.Models
public List<UserManagementPlexServers> Servers { get; set; }
}
public class CheckBox
{
public string Name { get; set; }
public int Value { get; set; }
public bool Selected { get; set; }
}
public class UserManagementPlexServers
{
public int Id { get; set; }
@ -50,8 +60,10 @@ namespace PlexRequests.UI.Models
public string Username { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
[JsonProperty("claims")]
public string[] Claims { get; set; }
[JsonProperty("permissions")]
public List<string> Permissions { get; set; }
[JsonProperty("features")]
public List<string> Features { get; set; }
[JsonProperty("email")]
public string EmailAddress { get; set; }
@ -61,20 +73,12 @@ namespace PlexRequests.UI.Models
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("claims")]
public List<ClaimsModel> Claims { get; set; }
[JsonProperty("permissions")]
public List<CheckBox> Permissions { get; set; }
[JsonProperty("features")]
public List<CheckBox> Features { get; set; }
public string Alias { get; set; }
public string EmailAddress { get; set; }
public class ClaimsModel
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("selected")]
public bool Selected { get; set; }
}
public string EmailAddress { get; set; }
}
}

View file

@ -56,6 +56,7 @@ using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Exceptions;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs;
using PlexRequests.Services.Notification;
@ -65,6 +66,8 @@ using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using Quartz;
using Action = PlexRequests.Helpers.Analytics.Action;
using HttpStatusCode = Nancy.HttpStatusCode;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
@ -122,7 +125,8 @@ namespace PlexRequests.UI.Modules
ICacheProvider cache, ISettingsService<SlackNotificationSettings> slackSettings,
ISlackApi slackApi, ISettingsService<LandingPageSettings> lp,
ISettingsService<ScheduledJobsSettings> scheduler, IJobRecord rec, IAnalytics analytics,
ISettingsService<NotificationSettingsV2> notifyService, IRecentlyAdded recentlyAdded) : base("admin", prService)
ISettingsService<NotificationSettingsV2> notifyService, IRecentlyAdded recentlyAdded
, ISecurityExtensions security) : base("admin", prService, security)
{
PrService = prService;
CpService = cpService;
@ -153,8 +157,8 @@ namespace PlexRequests.UI.Modules
NotifySettings = notifyService;
RecentlyAdded = recentlyAdded;
this.RequiresClaims(UserClaims.Admin);
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
Get["/"] = _ => Admin();
Get["/authentication", true] = async (x, ct) => await Authentication();
@ -185,7 +189,6 @@ namespace PlexRequests.UI.Modules
Get["/emailnotification"] = _ => EmailNotifications();
Post["/emailnotification"] = _ => SaveEmailNotifications();
Post["/testemailnotification", true] = async (x, ct) => await TestEmailNotifications();
Get["/status", true] = async (x, ct) => await Status();
Get["/pushbulletnotification"] = _ => PushbulletNotifications();
Post["/pushbulletnotification"] = _ => SavePushbulletNotifications();
@ -208,7 +211,7 @@ namespace PlexRequests.UI.Modules
Post["/createapikey"] = x => CreateApiKey();
Post["/autoupdate"] = x => AutoUpdate();
Post["/testslacknotification", true] = async (x, ct) => await TestSlackNotification();
@ -224,7 +227,7 @@ namespace PlexRequests.UI.Modules
Post["/clearlogs", true] = async (x, ct) => await ClearLogs();
Get["/notificationsettings", true] = async (x, ct) => await NotificationSettings();
Post["/notificationsettings", true] = async (x, ct) => await SaveNotificationSettings();
Post["/notificationsettings"] = x => SaveNotificationSettings();
Post["/recentlyAddedTest"] = x => RecentlyAddedTest();
}
@ -284,7 +287,7 @@ namespace PlexRequests.UI.Modules
{
Analytics.TrackEventAsync(Category.Admin, Action.Save, "CollectAnalyticData turned off", Username, CookieHelper.GetAnalyticClientId(Cookies));
}
var result = PrService.SaveSettings(model);
var result = await PrService.SaveSettingsAsync(model);
Analytics.TrackEventAsync(Category.Admin, Action.Save, "PlexRequestSettings", Username, CookieHelper.GetAnalyticClientId(Cookies));
return Response.AsJson(result
@ -403,9 +406,13 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(valid.SendJsonError());
}
//Lookup identifier
var server = PlexApi.GetServer(plexSettings.PlexAuthToken);
plexSettings.MachineIdentifier = server.Server.FirstOrDefault(x => x.AccessToken == plexSettings.PlexAuthToken)?.MachineIdentifier;
if (string.IsNullOrEmpty(plexSettings.MachineIdentifier))
{
//Lookup identifier
var server = PlexApi.GetServer(plexSettings.PlexAuthToken);
plexSettings.MachineIdentifier =
server.Server.FirstOrDefault(x => x.AccessToken == plexSettings.PlexAuthToken)?.MachineIdentifier;
}
var result = await PlexService.SaveSettingsAsync(plexSettings);
@ -513,7 +520,7 @@ namespace PlexRequests.UI.Modules
{
NotificationService.Subscribe(new EmailMessageNotification(EmailService));
settings.Enabled = true;
NotificationService.Publish(notificationModel, settings);
await NotificationService.Publish(notificationModel, settings);
Log.Info("Sent email notification test");
}
catch (Exception)
@ -563,28 +570,6 @@ namespace PlexRequests.UI.Modules
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private async Task<Negotiator> Status()
{
var checker = new StatusChecker();
var status = await Cache.GetOrSetAsync(CacheKeys.LastestProductVersion, async () => await checker.GetStatus(), 30);
var md = new Markdown(new MarkdownOptions { AutoNewLines = true, AutoHyperlink = true });
status.ReleaseNotes = md.Transform(status.ReleaseNotes);
return View["Status", status];
}
private Response AutoUpdate()
{
var url = Request.Form["url"];
var startInfo = Type.GetType("Mono.Runtime") != null
? new ProcessStartInfo("mono PlexRequests.Updater.exe") { Arguments = url }
: new ProcessStartInfo("PlexRequests.Updater.exe") { Arguments = url };
Process.Start(startInfo);
Environment.Exit(0);
return Nancy.Response.NoBody;
}
private Negotiator PushbulletNotifications()
{
@ -635,7 +620,7 @@ namespace PlexRequests.UI.Modules
{
NotificationService.Subscribe(new PushbulletNotification(PushbulletApi, PushbulletService));
settings.Enabled = true;
NotificationService.Publish(notificationModel, settings);
await NotificationService.Publish(notificationModel, settings);
Log.Info("Sent pushbullet notification test");
}
catch (Exception)
@ -701,7 +686,7 @@ namespace PlexRequests.UI.Modules
{
NotificationService.Subscribe(new PushoverNotification(PushoverApi, PushoverService));
settings.Enabled = true;
NotificationService.Publish(notificationModel, settings);
await NotificationService.Publish(notificationModel, settings);
Log.Info("Sent pushover notification test");
}
catch (Exception)
@ -757,7 +742,10 @@ namespace PlexRequests.UI.Modules
private Negotiator Logs()
{
return View["Logs"];
var model = false;
if (Request.Query["developer"] != null)
model = true;
return View["Logs", model];
}
private Response LoadLogs()
@ -844,12 +832,11 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(result
? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Newsletter!" }
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
}
private Response CreateApiKey()
{
this.RequiresClaims(UserClaims.Admin);
Analytics.TrackEventAsync(Category.Admin, Action.Create, "Created API Key", Username, CookieHelper.GetAnalyticClientId(Cookies));
var apiKey = Guid.NewGuid().ToString("N");
var settings = PrService.GetSettings();
@ -966,7 +953,24 @@ namespace PlexRequests.UI.Modules
{
var s = await ScheduledJobSettings.GetSettingsAsync();
var allJobs = await JobRecorder.GetJobsAsync();
var jobsDict = allJobs.ToDictionary(k => k.Name, v => v.LastRun);
var dict = new Dictionary<string, DateTime>();
foreach (var j in allJobs)
{
DateTime dt;
if (dict.TryGetValue(j.Name, out dt))
{
// We already have the key... Somehow, we should have never got this record.
}
else
{
dict.Add(j.Name,j.LastRun);
}
}
var model = new ScheduledJobsViewModel
{
CouchPotatoCacher = s.CouchPotatoCacher,
@ -975,8 +979,13 @@ namespace PlexRequests.UI.Modules
SonarrCacher = s.SonarrCacher,
StoreBackup = s.StoreBackup,
StoreCleanup = s.StoreCleanup,
JobRecorder = jobsDict,
RecentlyAddedCron = s.RecentlyAddedCron
JobRecorder = dict,
RecentlyAddedCron = s.RecentlyAddedCron,
PlexContentCacher = s.PlexContentCacher,
FaultQueueHandler = s.FaultQueueHandler,
PlexEpisodeCacher = s.PlexEpisodeCacher,
PlexUserChecker = s.PlexUserChecker,
UserRequestLimitResetter = s.UserRequestLimitResetter
};
return View["SchedulerSettings", model];
}
@ -995,11 +1004,11 @@ namespace PlexRequests.UI.Modules
if (!isValid)
{
return Response.AsJson(new JsonResponseModel
{
Result = false,
Message =
{
Result = false,
Message =
$"CRON {settings.RecentlyAddedCron} is not valid. Please ensure you are using a valid CRON."
});
});
}
}
var result = await ScheduledJobSettings.SaveSettingsAsync(settings);
@ -1034,7 +1043,7 @@ namespace PlexRequests.UI.Modules
return View["NotificationSettings", s];
}
private async Task<Negotiator> SaveNotificationSettings()
private Negotiator SaveNotificationSettings()
{
var model = this.Bind<NotificationSettingsV2>();
return View["NotificationSettings", model];
@ -1044,12 +1053,13 @@ namespace PlexRequests.UI.Modules
{
try
{
Log.Debug("Clicked TEST");
RecentlyAdded.Test();
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to administrator" });
}
catch (Exception e)
{
Log.Error(e);
return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message });
}
}

View file

@ -0,0 +1,75 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SystemStatusModule.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Linq;
using Nancy.Responses.Negotiation;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules.Admin
{
public class FaultQueueModule : BaseModule
{
public FaultQueueModule(ISettingsService<PlexRequestSettings> settingsService, IRepository<RequestQueue> requestQueue, ISecurityExtensions security) : base("admin", settingsService, security)
{
RequestQueue = requestQueue;
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
Get["Index", "/faultqueue"] = x => Index();
}
private IRepository<RequestQueue> RequestQueue { get; }
private Negotiator Index()
{
var requests = RequestQueue.GetAll();
var model = requests.Select(r => new FaultedRequestsViewModel
{
FaultType = (FaultTypeViewModel)(int)r.FaultType,
Type = (RequestTypeViewModel)(int)r.Type,
Title = ByteConverterHelper.ReturnObject<RequestedModel>(r.Content).Title,
Id = r.Id,
PrimaryIdentifier = r.PrimaryIdentifier,
LastRetry = r.LastRetry,
Message = r.Message
}).ToList();
return View["RequestFaultQueue", model];
}
}
}

View file

@ -0,0 +1,133 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SystemStatusModule.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using MarkdownSharp;
using Nancy;
using Nancy.ModelBinding;
using Nancy.Responses.Negotiation;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Core.StatusChecker;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules.Admin
{
public class SystemStatusModule : BaseModule
{
public SystemStatusModule(ISettingsService<PlexRequestSettings> settingsService, ICacheProvider cache, ISettingsService<SystemSettings> ss, ISecurityExtensions security, IAnalytics a) : base("admin", settingsService, security)
{
Cache = cache;
SystemSettings = ss;
Analytics = a;
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
Get["/status", true] = async (x, ct) => await Status();
Post["/save", true] = async (x, ct) => await Save();
Post["/autoupdate"] = x => AutoUpdate();
}
private ICacheProvider Cache { get; }
private ISettingsService<SystemSettings> SystemSettings { get; }
private IAnalytics Analytics { get; }
private async Task<Negotiator> Status()
{
var settings = await SystemSettings.GetSettingsAsync();
var checker = new StatusChecker(SystemSettings);
var status = await Cache.GetOrSetAsync(CacheKeys.LastestProductVersion, async () => await checker.GetStatus(), 30);
var md = new Markdown(new MarkdownOptions { AutoNewLines = true, AutoHyperlink = true });
status.ReleaseNotes = md.Transform(status.ReleaseNotes);
settings.Status = status;
settings.BranchDropdown = new List<BranchDropdown>
{
new BranchDropdown
{
Name = EnumHelper<Branches>.GetDisplayValue(Branches.Stable),
Value = Branches.Stable,
Selected = settings.Branch == Branches.Stable
},
new BranchDropdown
{
Name = EnumHelper<Branches>.GetDisplayValue(Branches.EarlyAccessPreview),
Value = Branches.EarlyAccessPreview,
Selected = settings.Branch == Branches.EarlyAccessPreview
},
new BranchDropdown
{
Name = EnumHelper<Branches>.GetDisplayValue(Branches.Dev),
Value = Branches.Dev,
Selected = settings.Branch == Branches.Dev
},
};
return View["Status", settings];
}
private async Task<Response> Save()
{
var settings = this.Bind<SystemSettings>();
Analytics.TrackEventAsync(Category.Admin, PlexRequests.Helpers.Analytics.Action.Update, $"Updated Branch {EnumHelper<Branches>.GetDisplayValue(settings.Branch)}", Username, CookieHelper.GetAnalyticClientId(Cookies));
await SystemSettings.SaveSettingsAsync(settings);
// Clear the cache
Cache.Remove(CacheKeys.LastestProductVersion);
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully Saved your settings"});
}
private Response AutoUpdate()
{
Analytics.TrackEventAsync(Category.Admin, PlexRequests.Helpers.Analytics.Action.Update, "AutoUpdate", Username, CookieHelper.GetAnalyticClientId(Cookies));
var url = Request.Form["url"];
var startInfo = Type.GetType("Mono.Runtime") != null
? new ProcessStartInfo("mono PlexRequests.Updater.exe") { Arguments = url }
: new ProcessStartInfo("PlexRequests.Updater.exe") { Arguments = url };
Process.Start(startInfo);
Environment.Exit(0);
return Nancy.Response.NoBody;
}
}
}

View file

@ -0,0 +1,85 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SystemStatusModule.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Threading.Tasks;
using Nancy;
using Nancy.ModelBinding;
using Nancy.Responses.Negotiation;
using Nancy.Validation;
using NLog;
using NLog.Fluent;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules.Admin
{
public class UserManagementSettingsModule : BaseModule
{
public UserManagementSettingsModule(ISettingsService<PlexRequestSettings> settingsService, ISettingsService<UserManagementSettings> umSettings, ISecurityExtensions security) : base("admin", settingsService, security)
{
UserManagementSettings = umSettings;
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
Get["UserManagementSettings","/usermanagementsettings", true] = async(x,ct) => await Index();
Post["/usermanagementsettings", true] = async(x,ct) => await Update();
}
private ISettingsService<UserManagementSettings> UserManagementSettings { get; }
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private async Task<Negotiator> Index()
{
var model = await UserManagementSettings.GetSettingsAsync();
return View["UserManagementSettings", model];
}
private async Task<Response> Update()
{
var settings = this.Bind<UserManagementSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
var error = valid.SendJsonError();
Log.Info("Error validating User Management settings, message: {0}", error.Message);
return Response.AsJson(error);
}
var result = await UserManagementSettings.SaveSettingsAsync(settings);
return Response.AsJson(result
? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for User Management!" }
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
}
}

View file

@ -29,12 +29,15 @@ using Nancy.Responses.Negotiation;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class ApiDocsModule : BaseModule
{
public ApiDocsModule(ISettingsService<PlexRequestSettings> pr) : base("apidocs", pr)
public ApiDocsModule(ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("apidocs", pr, security)
{
Get["/"] = x => Documentation();
}

View file

@ -36,14 +36,17 @@ using Newtonsoft.Json;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class ApiRequestModule : BaseApiModule
{
public ApiRequestModule(IRequestService service, ISettingsService<PlexRequestSettings> pr) : base("api", pr)
public ApiRequestModule(IRequestService service, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("api", pr, security)
{
Get["GetRequests","/requests"] = x => GetRequests();
Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x);

View file

@ -37,6 +37,9 @@ using Newtonsoft.Json;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
@ -44,7 +47,7 @@ namespace PlexRequests.UI.Modules
{
public ApiSettingsModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<AuthenticationSettings> auth,
ISettingsService<PlexSettings> plexSettings, ISettingsService<CouchPotatoSettings> cp,
ISettingsService<SonarrSettings> sonarr, ISettingsService<SickRageSettings> sr, ISettingsService<HeadphonesSettings> hp) : base("api", pr)
ISettingsService<SonarrSettings> sonarr, ISettingsService<SickRageSettings> sr, ISettingsService<HeadphonesSettings> hp, ISecurityExtensions security) : base("api", pr, security)
{
Get["GetVersion", "/version"] = x => GetVersion();

View file

@ -32,14 +32,17 @@ using Nancy.ModelBinding;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class ApiUserModule : BaseApiModule
{
public ApiUserModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m) : base("api", pr)
public ApiUserModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, ISecurityExtensions security) : base("api", pr, security)
{
Put["PutCredentials", "/credentials/{username}"] = x => ChangePassword(x);

View file

@ -36,10 +36,12 @@ using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
@ -47,7 +49,7 @@ namespace PlexRequests.UI.Modules
{
public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi,
ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr) : base("test", pr)
ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("test", pr, security)
{
this.RequiresAuthentication();

View file

@ -37,11 +37,14 @@ using NLog;
using PlexRequests.Api;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core;
using PlexRequests.Core.Queue;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
@ -50,9 +53,11 @@ namespace PlexRequests.UI.Modules
public ApprovalModule(IRequestService service, ISettingsService<CouchPotatoSettings> cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi,
ISettingsService<SonarrSettings> sonarrSettings, ISickRageApi srApi, ISettingsService<SickRageSettings> srSettings,
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr) : base("approval", pr)
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr, ITransientFaultQueue faultQueue
, ISecurityExtensions security) : base("approval", pr, security)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
Before += (ctx) => Security.AdminLoginRedirect(ctx, Permissions.Administrator,Permissions.ManageRequests);
Service = service;
CpService = cpService;
@ -63,6 +68,7 @@ namespace PlexRequests.UI.Modules
SickRageSettings = srSettings;
HeadphonesSettings = hpSettings;
HeadphoneApi = hpApi;
FaultQueue = faultQueue;
Post["/approve", true] = async (x, ct) => await Approve((int)Request.Form.requestid, (string)Request.Form.qualityId);
Post["/deny", true] = async (x, ct) => await DenyRequest((int)Request.Form.requestid, (string)Request.Form.reason);
@ -85,6 +91,7 @@ namespace PlexRequests.UI.Modules
private ISickRageApi SickRageApi { get; }
private ICouchPotatoApi CpApi { get; }
private IHeadphonesApi HeadphoneApi { get; }
private ITransientFaultQueue FaultQueue { get; }
/// <summary>
/// Approves the specified request identifier.

View file

@ -32,19 +32,22 @@ using Nancy.Validation;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public abstract class BaseApiModule : BaseModule
{
protected BaseApiModule(ISettingsService<PlexRequestSettings> s) : base(s)
protected BaseApiModule(ISettingsService<PlexRequestSettings> s, ISecurityExtensions security) : base(s,security)
{
Settings = s;
Before += (ctx) => CheckAuth();
}
protected BaseApiModule(string modulePath, ISettingsService<PlexRequestSettings> s) : base(modulePath, s)
protected BaseApiModule(string modulePath, ISettingsService<PlexRequestSettings> s, ISecurityExtensions security) : base(modulePath, s, security)
{
Settings = s;
Before += (ctx) => CheckAuth();

View file

@ -28,21 +28,22 @@
using Nancy;
using Nancy.Extensions;
using PlexRequests.UI.Models;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public abstract class BaseAuthModule : BaseModule
{
protected BaseAuthModule(ISettingsService<PlexRequestSettings> pr) : base(pr)
protected BaseAuthModule(ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base(pr,security)
{
PlexRequestSettings = pr;
Before += (ctx) => CheckAuth();
}
protected BaseAuthModule(string modulePath, ISettingsService<PlexRequestSettings> pr) : base(modulePath, pr)
protected BaseAuthModule(string modulePath, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base(modulePath, pr, security)
{
PlexRequestSettings = pr;
Before += (ctx) => CheckAuth();
@ -61,12 +62,14 @@ namespace PlexRequests.UI.Modules
{
return Context.GetRedirect(string.IsNullOrEmpty(baseUrl) ? "~/wizard" : $"~/{baseUrl}/wizard");
}
var redirectPath = string.IsNullOrEmpty(baseUrl) ? "~/userlogin" : $"~/{baseUrl}/userlogin";
if (Session[SessionKeys.UsernameKey] == null && Context?.CurrentUser == null)
if (!Request.IsAjaxRequest())
{
return Context.GetRedirect(redirectPath);
var redirectPath = string.IsNullOrEmpty(baseUrl) ? "~/userlogin" : $"~/{baseUrl}/userlogin";
if (Session[SessionKeys.UsernameKey] == null && Context?.CurrentUser == null)
{
return Context.GetRedirect(redirectPath);
}
}
return null;

View file

@ -30,12 +30,13 @@ using System.Linq;
using System.Threading;
using Nancy;
using Nancy.Security;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
@ -44,7 +45,7 @@ namespace PlexRequests.UI.Modules
protected string BaseUrl { get; set; }
protected BaseModule(ISettingsService<PlexRequestSettings> settingsService)
protected BaseModule(ISettingsService<PlexRequestSettings> settingsService, ISecurityExtensions security)
{
var settings = settingsService.GetSettings();
@ -54,11 +55,12 @@ namespace PlexRequests.UI.Modules
var modulePath = string.IsNullOrEmpty(baseUrl) ? string.Empty : baseUrl;
ModulePath = modulePath;
Security = security;
Before += (ctx) => SetCookie();
}
protected BaseModule(string modulePath, ISettingsService<PlexRequestSettings> settingsService)
protected BaseModule(string modulePath, ISettingsService<PlexRequestSettings> settingsService, ISecurityExtensions security)
{
var settings = settingsService.GetSettings();
@ -68,6 +70,7 @@ namespace PlexRequests.UI.Modules
var settingModulePath = string.IsNullOrEmpty(baseUrl) ? modulePath : $"{baseUrl}/{modulePath}";
ModulePath = settingModulePath;
Security = security;
Before += (ctx) =>
{
@ -95,8 +98,12 @@ namespace PlexRequests.UI.Modules
return _dateTimeOffset;
}
}
private string _username;
private string _username;
/// <summary>
/// Returns the Username or UserAlias
/// </summary>
protected string Username
{
get
@ -105,7 +112,12 @@ namespace PlexRequests.UI.Modules
{
try
{
_username = Session[SessionKeys.UsernameKey].ToString();
var username = Security.GetUsername(User.UserName);
if (string.IsNullOrEmpty(username))
{
return Session[SessionKeys.UsernameKey].ToString();
}
_username = username;
}
catch (Exception)
{
@ -126,11 +138,15 @@ namespace PlexRequests.UI.Modules
{
return false;
}
var claims = Context?.CurrentUser.Claims.ToList();
return claims.Contains(UserClaims.Admin) || claims.Contains(UserClaims.PowerUser);
return Security.HasPermissions(Context?.CurrentUser, Permissions.Administrator);
}
}
protected IUserIdentity User => Context?.CurrentUser;
protected ISecurityExtensions Security { get; set; }
protected bool LoggedIn => Context?.CurrentUser != null;
protected string Culture { get; set; }

View file

@ -35,22 +35,24 @@ using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class CultureModule : BaseModule
{
public CultureModule(ISettingsService<PlexRequestSettings> pr, IAnalytics a) : base("culture",pr)
public CultureModule(ISettingsService<PlexRequestSettings> pr, IAnalytics a, ISecurityExtensions security) : base("culture",pr, security)
{
Analytics = a;
Get["/", true] = async(x,c) => await SetCulture();
Get["/"] = x => SetCulture();
}
private IAnalytics Analytics { get; }
public async Task<RedirectResponse> SetCulture()
private RedirectResponse SetCulture()
{
var culture = (string)Request.Query["l"];
var returnUrl = (string)Request.Query["u"];

View file

@ -2,20 +2,20 @@
using System.Threading.Tasks;
using Nancy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.UI.Models;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class DonationLinkModule : BaseAuthModule
{
public DonationLinkModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr) : base("customDonation", pr)
public DonationLinkModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("customDonation", pr, security)
{
Cache = provider;
@ -33,11 +33,11 @@ namespace PlexRequests.UI.Modules
{
if (settings.EnableCustomDonationUrl)
{
return Response.AsJson(new { url = settings.CustomDonationUrl, message = settings.CustomDonationMessage });
return Response.AsJson(new { url = settings.CustomDonationUrl, message = settings.CustomDonationMessage, enabled = true });
}
else
{
return Response.AsJson(new { url = settings.CustomDonationUrl, message = settings.CustomDonationMessage });
return Response.AsJson(new { enabled = false });
}
}
catch (Exception e)

View file

@ -33,12 +33,15 @@ using Nancy.Responses;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class IndexModule : BaseAuthModule
{
public IndexModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<LandingPageSettings> l, IResourceLinker rl) : base(pr)
public IndexModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<LandingPageSettings> l, IResourceLinker rl, ISecurityExtensions security) : base(pr, security)
{
LandingPage = l;
Linker = rl;
@ -56,7 +59,7 @@ namespace PlexRequests.UI.Modules
{
if (settings.BeforeLogin) // Before login
{
if (!string.IsNullOrEmpty(Username))
if (string.IsNullOrEmpty(Username))
{
// They are not logged in
return Context.GetRedirect(Linker.BuildRelativeUri(Context, "LandingPageIndex").ToString());

View file

@ -15,17 +15,19 @@ using PlexRequests.Core;
using PlexRequests.Core.Models;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class IssuesModule : BaseAuthModule
{
public IssuesModule(ISettingsService<PlexRequestSettings> pr, IIssueService issueService, IRequestService request, INotificationService n) : base("issues", pr)
public IssuesModule(ISettingsService<PlexRequestSettings> pr, IIssueService issueService, IRequestService request, INotificationService n, ISecurityExtensions security) : base("issues", pr, security)
{
IssuesService = issueService;
RequestService = request;
@ -78,7 +80,8 @@ namespace PlexRequests.UI.Modules
foreach (var i in issuesModels)
{
var model = new IssuesViewModel { Id = i.Id, RequestId = i.RequestId, Title = i.Title, Type = i.Type.ToString().ToCamelCaseWords(), Admin = IsAdmin };
var model = new IssuesViewModel { Id = i.Id, RequestId = i.RequestId, Title = i.Title, Type = i.Type.ToString().ToCamelCaseWords(), Admin = Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests)
};
// Create a string with all of the current issue states with a "," delimiter in e.g. Wrong Content, Playback Issues
var state = i.Issues.Select(x => x.Issue).ToArray();
@ -332,7 +335,7 @@ namespace PlexRequests.UI.Modules
myIssues = issuesModels.Where(x => x.IssueStatus != IssueStatus.ResolvedIssue);
}
}
else if (settings.UsersCanViewOnlyOwnIssues) // The user is not an Admin, do we have the settings to hide them?
else if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnIssues)) // The user is not an Admin, do we have the settings to hide them?
{
if (!showResolved)
{
@ -366,7 +369,11 @@ namespace PlexRequests.UI.Modules
{
try
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
if (!Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Sorry, you do not have the correct permissions to remove an issue." });
}
var issue = await IssuesService.GetAsync(issueId);
var request = await RequestService.GetAsync(issue.RequestId);
if (request.Id > 0)
@ -399,7 +406,11 @@ namespace PlexRequests.UI.Modules
{
try
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
if (!Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests))
{
return View["Index"];
}
var issue = await IssuesService.GetAsync(issueId);
issue.IssueStatus = status;
@ -417,7 +428,11 @@ namespace PlexRequests.UI.Modules
private async Task<Negotiator> ClearIssue(int issueId, IssueState state)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
if (!Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests))
{
return View["Index"];
}
var issue = await IssuesService.GetAsync(issueId);
var toRemove = issue.Issues.FirstOrDefault(x => x.Issue == state);
@ -430,7 +445,11 @@ namespace PlexRequests.UI.Modules
private async Task<Response> AddNote(int requestId, string noteArea, IssueState state)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
if (!Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Sorry, you do not have the correct permissions to add a note." });
}
var issue = await IssuesService.GetAsync(requestId);
if (issue == null)
{

View file

@ -33,14 +33,17 @@ using Nancy.Linker;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class LandingPageModule : BaseModule
{
public LandingPageModule(ISettingsService<PlexRequestSettings> settingsService, ISettingsService<LandingPageSettings> landing,
ISettingsService<PlexSettings> ps, IPlexApi pApi, IResourceLinker linker) : base("landing", settingsService)
ISettingsService<PlexSettings> ps, IPlexApi pApi, IResourceLinker linker, ISecurityExtensions security) : base("landing", settingsService, security)
{
LandingSettings = landing;
PlexSettings = ps;

View file

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UpdateCheckerModule.cs
// File: LayoutModule.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -25,6 +25,7 @@
// ************************************************************************/
#endregion
using System;
using System.Linq;
using System.Threading.Tasks;
using Nancy;
@ -33,23 +34,34 @@ using NLog;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Core.StatusChecker;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class UpdateCheckerModule : BaseAuthModule
public class LayoutModule : BaseAuthModule
{
public UpdateCheckerModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr) : base("updatechecker", pr)
public LayoutModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr, ISettingsService<SystemSettings> settings, IJobRecord rec, ISecurityExtensions security) : base("layout", pr, security)
{
Cache = provider;
SystemSettings = settings;
Job = rec;
Get["/", true] = async (x,ct) => await CheckLatestVersion();
Get["/cacher", true] = async (x,ct) => await CacherRunning();
}
private ICacheProvider Cache { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
private ISettingsService<SystemSettings> SystemSettings { get; }
private IJobRecord Job { get; }
private async Task<Response> CheckLatestVersion()
{
@ -59,10 +71,10 @@ namespace PlexRequests.UI.Modules
{
return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = false });
}
#if DEBUG
return Response.AsJson(new JsonUpdateAvailableModel {UpdateAvailable = false});
#endif
var checker = new StatusChecker();
//#if DEBUG
//return Response.AsJson(new JsonUpdateAvailableModel {UpdateAvailable = false});
//#endif
var checker = new StatusChecker(SystemSettings);
var release = await Cache.GetOrSetAsync(CacheKeys.LastestProductVersion, async() => await checker.GetStatus(), 30);
return Response.AsJson(release.UpdateAvailable
@ -76,5 +88,36 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = false });
}
}
private async Task<Response> CacherRunning()
{
try
{
var jobs = await Job.GetJobsAsync();
// Check to see if any are running
var runningJobs = jobs.Where(x => x.Running);
// We only want the cachers
var cacherJobs = runningJobs.Where(x =>
x.Name.Equals(JobNames.CpCacher)
|| x.Name.Equals(JobNames.EpisodeCacher)
|| x.Name.Equals(JobNames.PlexChecker)
|| x.Name.Equals(JobNames.SonarrCacher)
|| x.Name.Equals(JobNames.SrCacher)
|| x.Name.Equals(JobNames.PlexCacher));
return Response.AsJson(cacherJobs.Any()
? new { CurrentlyRunning = true, IsAdmin}
: new { CurrentlyRunning = false, IsAdmin });
}
catch (Exception e)
{
Log.Warn("Exception Thrown when attempting to check the status");
Log.Warn(e);
return Response.AsJson(new { CurrentlyRunning = false, IsAdmin });
}
}
}
}

View file

@ -31,7 +31,6 @@ using System;
using System.Dynamic;
using System.Security;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Extensions;
using Nancy.Linker;
using Nancy.Responses.Negotiation;
@ -40,19 +39,22 @@ using Nancy.Security;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Authentication;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class LoginModule : BaseModule
{
public LoginModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IResourceLinker linker, IRepository<UserLogins> userLoginRepo)
: base(pr)
public LoginModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IResourceLinker linker, IRepository<UserLogins> userLoginRepo, ISecurityExtensions security)
: base(pr, security)
{
UserMapper = m;
Get["/login"] = _ =>
Get["LocalLogin","/login"] = _ =>
{
if (LoggedIn)
{
@ -73,7 +75,7 @@ namespace PlexRequests.UI.Modules
{
Session.Delete(SessionKeys.UsernameKey);
}
return this.LogoutAndRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/" : "~/");
return CustomModuleExtensions.LogoutAndRedirect(this, !string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/" : "~/");
};
Post["/login"] = x =>
@ -111,7 +113,7 @@ namespace PlexRequests.UI.Modules
UserId = userId.ToString()
});
return this.LoginAndRedirect(userId.Value, expiry, redirect);
return CustomModuleExtensions.LoginAndRedirect(this,userId.Value, expiry, redirect);
};
Get["/register"] = x =>
@ -135,9 +137,9 @@ namespace PlexRequests.UI.Modules
? $"~/{BaseUrl}/register?error=true"
: "~/register?error=true");
}
var userId = UserMapper.CreateAdmin(username, Request.Form.Password);
var userId = UserMapper.CreateUser(username, Request.Form.Password, EnumHelper<Permissions>.All(), 0);
Session[SessionKeys.UsernameKey] = username;
return this.LoginAndRedirect((Guid)userId);
return CustomModuleExtensions.LoginAndRedirect(this, (Guid)userId);
};
Get["/changepassword"] = _ => ChangePassword();

View file

@ -1,453 +0,0 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: RequestsModule.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Linq;
using Nancy;
using Nancy.Responses.Negotiation;
using Nancy.Security;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification;
using PlexRequests.Store;
using PlexRequests.UI.Models;
using PlexRequests.Helpers;
using PlexRequests.UI.Helpers;
using System.Collections.Generic;
using PlexRequests.Api.Interfaces;
using System.Threading.Tasks;
using NLog;
using PlexRequests.Core.Models;
using PlexRequests.Helpers.Analytics;
using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.UI.Modules
{
public class RequestsBetaModule : BaseAuthModule
{
public RequestsBetaModule(
IRequestService service,
ISettingsService<PlexRequestSettings> prSettings,
ISettingsService<RequestSettings> requestSettings,
ISettingsService<PlexSettings> plex,
INotificationService notify,
ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<SickRageSettings> sickRageSettings,
ISettingsService<CouchPotatoSettings> cpSettings,
ICouchPotatoApi cpApi,
ISonarrApi sonarrApi,
ISickRageApi sickRageApi,
ICacheProvider cache,
IAnalytics an) : base("requestsbeta", prSettings)
{
Service = service;
PrSettings = prSettings;
PlexSettings = plex;
NotificationService = notify;
SonarrSettings = sonarrSettings;
SickRageSettings = sickRageSettings;
CpSettings = cpSettings;
SonarrApi = sonarrApi;
SickRageApi = sickRageApi;
CpApi = cpApi;
Cache = cache;
Analytics = an;
Get["/"] = x => LoadRequests();
Get["/plexrequestsettings", true] = async (x, ct) => await GetPlexRequestSettings();
Get["/requestsettings", true] = async (x, ct) => await GetRequestSettings();
Get["/movies", true] = async (x, ct) => await GetMovies();
Get["/movies/{searchTerm}", true] = async (x, ct) => await GetMovies((string)x.searchTerm);
// Everything below is not being used in the beta page
Get["/tvshows", true] = async (c, ct) => await GetTvShows();
Get["/albums", true] = async (x, ct) => await GetAlbumRequests();
Post["/delete", true] = async (x, ct) => await DeleteRequest((int)Request.Form.id);
Post["/reportissue", true] = async (x, ct) => await ReportIssue((int)Request.Form.requestId, (IssueState)(int)Request.Form.issue, null);
Post["/reportissuecomment", true] = async (x, ct) => await ReportIssue((int)Request.Form.requestId, IssueState.Other, (string)Request.Form.commentArea);
Post["/clearissues", true] = async (x, ct) => await ClearIssue((int)Request.Form.Id);
Post["/changeavailability", true] = async (x, ct) => await ChangeRequestAvailability((int)Request.Form.Id, (bool)Request.Form.Available);
}
private static Logger Log = LogManager.GetCurrentClassLogger();
private IRequestService Service { get; }
private IAnalytics Analytics { get; }
private INotificationService NotificationService { get; }
private ISettingsService<PlexRequestSettings> PrSettings { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<RequestSettings> RequestSettings { get; }
private ISettingsService<SonarrSettings> SonarrSettings { get; }
private ISettingsService<SickRageSettings> SickRageSettings { get; }
private ISettingsService<CouchPotatoSettings> CpSettings { get; }
private ISonarrApi SonarrApi { get; }
private ISickRageApi SickRageApi { get; }
private ICouchPotatoApi CpApi { get; }
private ICacheProvider Cache { get; }
private Negotiator LoadRequests()
{
return View["Index"];
}
private async Task<Response> GetPlexRequestSettings()
{
return Response.AsJson(await PrSettings.GetSettingsAsync());
}
private async Task<Response> GetRequestSettings()
{
return Response.AsJson(await RequestSettings.GetSettingsAsync());
}
private async Task<Response> GetMovies(string searchTerm = null, bool approved = false, bool notApproved = false,
bool available = false, bool notAvailable = false, bool released = false, bool notReleased = false)
{
var dbMovies = await FilterMovies(searchTerm, approved, notApproved, available, notAvailable, released, notReleased);
var qualities = await GetQualityProfiles();
var model = MapMoviesToView(dbMovies.ToList(), qualities);
return Response.AsJson(model);
}
private async Task<Response> GetTvShows()
{
var settingsTask = PrSettings.GetSettingsAsync();
var requests = await Service.GetAllAsync();
requests = requests.Where(x => x.Type == RequestType.TvShow);
var dbTv = requests;
var settings = await settingsTask;
if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin)
{
dbTv = dbTv.Where(x => x.UserHasRequested(Username)).ToList();
}
IEnumerable<QualityModel> qualities = new List<QualityModel>();
if (IsAdmin)
{
try
{
var sonarrSettings = await SonarrSettings.GetSettingsAsync();
if (sonarrSettings.Enabled)
{
var result = Cache.GetOrSetAsync(CacheKeys.SonarrQualityProfiles, async () =>
{
return await Task.Run(() => SonarrApi.GetProfiles(sonarrSettings.ApiKey, sonarrSettings.FullUri));
});
qualities = result.Result.Select(x => new QualityModel { Id = x.id.ToString(), Name = x.name }).ToList();
}
else
{
var sickRageSettings = await SickRageSettings.GetSettingsAsync();
if (sickRageSettings.Enabled)
{
qualities = sickRageSettings.Qualities.Select(x => new QualityModel { Id = x.Key, Name = x.Value }).ToList();
}
}
}
catch (Exception e)
{
Log.Info(e);
}
}
var viewModel = dbTv.Select(tv => new RequestViewModel
{
ProviderId = tv.ProviderId,
Type = tv.Type,
Status = tv.Status,
ImdbId = tv.ImdbId,
Id = tv.Id,
PosterPath = tv.PosterPath,
ReleaseDate = tv.ReleaseDate,
ReleaseDateTicks = tv.ReleaseDate.Ticks,
RequestedDate = tv.RequestedDate,
RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(tv.RequestedDate, DateTimeOffset).Ticks,
Released = DateTime.Now > tv.ReleaseDate,
Approved = tv.Available || tv.Approved,
Title = tv.Title,
Overview = tv.Overview,
RequestedUsers = IsAdmin ? tv.AllUsers.ToArray() : new string[] { },
ReleaseYear = tv.ReleaseDate.Year.ToString(),
Available = tv.Available,
Admin = IsAdmin,
IssueId = tv.IssueId,
TvSeriesRequestType = tv.SeasonsRequested,
Qualities = qualities.ToArray(),
Episodes = tv.Episodes.ToArray(),
}).ToList();
return Response.AsJson(viewModel);
}
private async Task<Response> GetAlbumRequests()
{
var settings = PrSettings.GetSettings();
var dbAlbum = await Service.GetAllAsync();
dbAlbum = dbAlbum.Where(x => x.Type == RequestType.Album);
if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin)
{
dbAlbum = dbAlbum.Where(x => x.UserHasRequested(Username));
}
var viewModel = dbAlbum.Select(album =>
{
return new RequestViewModel
{
ProviderId = album.ProviderId,
Type = album.Type,
Status = album.Status,
ImdbId = album.ImdbId,
Id = album.Id,
PosterPath = album.PosterPath,
ReleaseDate = album.ReleaseDate,
ReleaseDateTicks = album.ReleaseDate.Ticks,
RequestedDate = album.RequestedDate,
RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(album.RequestedDate, DateTimeOffset).Ticks,
Released = DateTime.Now > album.ReleaseDate,
Approved = album.Available || album.Approved,
Title = album.Title,
Overview = album.Overview,
RequestedUsers = IsAdmin ? album.AllUsers.ToArray() : new string[] { },
ReleaseYear = album.ReleaseDate.Year.ToString(),
Available = album.Available,
Admin = IsAdmin,
IssueId = album.IssueId,
TvSeriesRequestType = album.SeasonsRequested,
MusicBrainzId = album.MusicBrainzId,
ArtistName = album.ArtistName
};
}).ToList();
return Response.AsJson(viewModel);
}
private async Task<Response> DeleteRequest(int requestid)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
Analytics.TrackEventAsync(Category.Requests, Action.Delete, "Delete Request", Username, CookieHelper.GetAnalyticClientId(Cookies));
var currentEntity = await Service.GetAsync(requestid);
await Service.DeleteRequestAsync(currentEntity);
return Response.AsJson(new JsonResponseModel { Result = true });
}
/// <summary>
/// Reports the issue.
/// Comment can be null if the <c>IssueState != Other</c>
/// </summary>
/// <param name="requestId">The request identifier.</param>
/// <param name="issue">The issue.</param>
/// <param name="comment">The comment.</param>
/// <returns></returns>
private async Task<Response> ReportIssue(int requestId, IssueState issue, string comment)
{
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not add issue, please try again or contact the administrator!" });
}
originalRequest.Issues = issue;
originalRequest.OtherMessage = !string.IsNullOrEmpty(comment)
? $"{Username} - {comment}"
: string.Empty;
var result = await Service.UpdateRequestAsync(originalRequest);
var model = new NotificationModel
{
User = Username,
NotificationType = NotificationType.Issue,
Title = originalRequest.Title,
DateTime = DateTime.Now,
Body = issue == IssueState.Other ? comment : issue.ToString().ToCamelCaseWords()
};
await NotificationService.Publish(model);
return Response.AsJson(result
? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "Could not add issue, please try again or contact the administrator!" });
}
private async Task<Response> ClearIssue(int requestId)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Request does not exist to clear it!" });
}
originalRequest.Issues = IssueState.None;
originalRequest.OtherMessage = string.Empty;
var result = await Service.UpdateRequestAsync(originalRequest);
return Response.AsJson(result
? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "Could not clear issue, please try again or check the logs" });
}
private async Task<Response> ChangeRequestAvailability(int requestId, bool available)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
Analytics.TrackEventAsync(Category.Requests, Action.Update, available ? "Make request available" : "Make request unavailable", Username, CookieHelper.GetAnalyticClientId(Cookies));
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Request does not exist to change the availability!" });
}
originalRequest.Available = available;
var result = await Service.UpdateRequestAsync(originalRequest);
return Response.AsJson(result
? new { Result = true, Available = available, Message = string.Empty }
: new { Result = false, Available = false, Message = "Could not update the availability, please try again or check the logs" });
}
private List<RequestViewModel> MapMoviesToView(List<RequestedModel> dbMovies, List<QualityModel> qualities)
{
return dbMovies.Select(movie => new RequestViewModel
{
ProviderId = movie.ProviderId,
Type = movie.Type,
Status = movie.Status,
ImdbId = movie.ImdbId,
Id = movie.Id,
PosterPath = movie.PosterPath,
ReleaseDate = movie.ReleaseDate,
ReleaseDateTicks = movie.ReleaseDate.Ticks,
RequestedDate = movie.RequestedDate,
Released = DateTime.Now > movie.ReleaseDate,
RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).Ticks,
Approved = movie.Available || movie.Approved,
Title = movie.Title,
Overview = movie.Overview,
RequestedUsers = IsAdmin ? movie.AllUsers.ToArray() : new string[] { },
ReleaseYear = movie.ReleaseDate.Year.ToString(),
Available = movie.Available,
Admin = IsAdmin,
IssueId = movie.IssueId,
Qualities = qualities.ToArray()
}).ToList();
}
private async Task<List<QualityModel>> GetQualityProfiles()
{
var qualities = new List<QualityModel>();
if (IsAdmin)
{
var cpSettings = CpSettings.GetSettings();
if (cpSettings.Enabled)
{
try
{
var result = await Cache.GetOrSetAsync(CacheKeys.CouchPotatoQualityProfiles, async () =>
{
return await Task.Run(() => CpApi.GetProfiles(cpSettings.FullUri, cpSettings.ApiKey)).ConfigureAwait(false);
});
if (result != null)
{
qualities = result.list.Select(x => new QualityModel { Id = x._id, Name = x.label }).ToList();
}
}
catch (Exception e)
{
Log.Info(e);
}
}
}
return qualities;
}
private async Task<IEnumerable<RequestedModel>> FilterMovies(string searchTerm = null, bool approved = false, bool notApproved = false,
bool available = false, bool notAvailable = false, bool released = false, bool notReleased = false)
{
var settings = PrSettings.GetSettings();
var allRequests = await Service.GetAllAsync();
allRequests = allRequests.Where(x => x.Type == RequestType.Movie);
var dbMovies = allRequests;
if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin)
{
dbMovies = dbMovies.Where(x => x.UserHasRequested(Username));
}
// Filter the movies on the search term
if (!string.IsNullOrEmpty(searchTerm))
{
dbMovies = dbMovies.Where(x => x.Title.Contains(searchTerm));
}
if (approved)
{
dbMovies = dbMovies.Where(x => x.Approved);
}
if (notApproved)
{
dbMovies = dbMovies.Where(x => !x.Approved);
}
if (available)
{
dbMovies = dbMovies.Where(x => x.Available);
}
if (notAvailable)
{
dbMovies = dbMovies.Where(x => !x.Available);
}
if (released)
{
dbMovies = dbMovies.Where(x => DateTime.Now > x.ReleaseDate);
}
if (notReleased)
{
dbMovies = dbMovies.Where(x => DateTime.Now < x.ReleaseDate);
}
return dbMovies;
}
}
}

View file

@ -39,7 +39,6 @@ using PlexRequests.Services.Notification;
using PlexRequests.Store;
using PlexRequests.UI.Models;
using PlexRequests.Helpers;
using PlexRequests.UI.Helpers;
using System.Collections.Generic;
using PlexRequests.Api.Interfaces;
using System.Threading.Tasks;
@ -48,8 +47,10 @@ using NLog;
using PlexRequests.Core.Models;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using Action = PlexRequests.Helpers.Analytics.Action;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
@ -68,7 +69,8 @@ namespace PlexRequests.UI.Modules
ISickRageApi sickRageApi,
ICacheProvider cache,
IAnalytics an,
INotificationEngine engine) : base("requests", prSettings)
INotificationEngine engine,
ISecurityExtensions security) : base("requests", prSettings, security)
{
Service = service;
PrSettings = prSettings;
@ -127,7 +129,7 @@ namespace PlexRequests.UI.Modules
var dbMovies = allRequests.ToList();
if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin)
if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) && !IsAdmin)
{
dbMovies = dbMovies.Where(x => x.UserHasRequested(Username)).ToList();
}
@ -157,6 +159,8 @@ namespace PlexRequests.UI.Modules
}
}
var canManageRequest = Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests);
var viewModel = dbMovies.Select(movie => new RequestViewModel
{
ProviderId = movie.ProviderId,
@ -173,10 +177,10 @@ namespace PlexRequests.UI.Modules
Approved = movie.Available || movie.Approved,
Title = movie.Title,
Overview = movie.Overview,
RequestedUsers = IsAdmin ? movie.AllUsers.ToArray() : new string[] { },
RequestedUsers = canManageRequest ? movie.AllUsers.ToArray() : new string[] { },
ReleaseYear = movie.ReleaseDate.Year.ToString(),
Available = movie.Available,
Admin = IsAdmin,
Admin = canManageRequest,
IssueId = movie.IssueId,
Denied = movie.Denied,
DeniedReason = movie.DeniedReason,
@ -195,7 +199,7 @@ namespace PlexRequests.UI.Modules
var dbTv = requests;
var settings = await settingsTask;
if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin)
if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) && !IsAdmin)
{
dbTv = dbTv.Where(x => x.UserHasRequested(Username)).ToList();
}
@ -230,6 +234,7 @@ namespace PlexRequests.UI.Modules
}
var canManageRequest = Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests);
var viewModel = dbTv.Select(tv => new RequestViewModel
{
ProviderId = tv.ProviderId,
@ -246,10 +251,10 @@ namespace PlexRequests.UI.Modules
Approved = tv.Available || tv.Approved,
Title = tv.Title,
Overview = tv.Overview,
RequestedUsers = IsAdmin ? tv.AllUsers.ToArray() : new string[] { },
RequestedUsers = canManageRequest ? tv.AllUsers.ToArray() : new string[] { },
ReleaseYear = tv.ReleaseDate.Year.ToString(),
Available = tv.Available,
Admin = IsAdmin,
Admin = canManageRequest,
IssueId = tv.IssueId,
Denied = tv.Denied,
DeniedReason = tv.DeniedReason,
@ -266,11 +271,11 @@ namespace PlexRequests.UI.Modules
var settings = PrSettings.GetSettings();
var dbAlbum = await Service.GetAllAsync();
dbAlbum = dbAlbum.Where(x => x.Type == RequestType.Album);
if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin)
if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) && !IsAdmin)
{
dbAlbum = dbAlbum.Where(x => x.UserHasRequested(Username));
}
var canManageRequest = Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests);
var viewModel = dbAlbum.Select(album =>
{
return new RequestViewModel
@ -289,10 +294,10 @@ namespace PlexRequests.UI.Modules
Approved = album.Available || album.Approved,
Title = album.Title,
Overview = album.Overview,
RequestedUsers = IsAdmin ? album.AllUsers.ToArray() : new string[] { },
RequestedUsers = canManageRequest ? album.AllUsers.ToArray() : new string[] { },
ReleaseYear = album.ReleaseDate.Year.ToString(),
Available = album.Available,
Admin = IsAdmin,
Admin = canManageRequest,
IssueId = album.IssueId,
Denied = album.Denied,
DeniedReason = album.DeniedReason,
@ -308,7 +313,12 @@ namespace PlexRequests.UI.Modules
private async Task<Response> DeleteRequest(int requestid)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
if (!Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests))
{
return Response.AsJson(new JsonResponseModel { Result = true });
}
Analytics.TrackEventAsync(Category.Requests, Action.Delete, "Delete Request", Username, CookieHelper.GetAnalyticClientId(Cookies));
var currentEntity = await Service.GetAsync(requestid);
@ -326,6 +336,10 @@ namespace PlexRequests.UI.Modules
/// <returns></returns>
private async Task<Response> ReportIssue(int requestId, IssueState issue, string comment)
{
if (!Security.HasPermissions(User, Permissions.ReportIssue))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Sorry, you do not have the correct permissions to report an issue." });
}
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)
{
@ -356,7 +370,10 @@ namespace PlexRequests.UI.Modules
private async Task<Response> ClearIssue(int requestId)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
if (!Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Sorry, you do not have the correct permissions to clear an issue." });
}
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)
@ -374,7 +391,11 @@ namespace PlexRequests.UI.Modules
private async Task<Response> ChangeRequestAvailability(int requestId, bool available)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
if (!Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Sorry, you do not have the correct permissions to change a request." });
}
Analytics.TrackEventAsync(Category.Requests, Action.Update, available ? "Make request available" : "Make request unavailable", Username, CookieHelper.GetAnalyticClientId(Cookies));
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)
@ -386,7 +407,7 @@ namespace PlexRequests.UI.Modules
var result = await Service.UpdateRequestAsync(originalRequest);
var plexService = await PlexSettings.GetSettingsAsync();
await NotificationEngine.NotifyUsers(originalRequest, plexService.PlexAuthToken);
await NotificationEngine.NotifyUsers(originalRequest, plexService.PlexAuthToken, available ? NotificationType.RequestAvailable : NotificationType.RequestDeclined);
return Response.AsJson(result
? new { Result = true, Available = available, Message = string.Empty }
: new { Result = false, Available = false, Message = "Could not update the availability, please try again or check the logs" });

View file

@ -1,4 +1,5 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SearchModule.cs
@ -23,7 +24,9 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
@ -54,15 +57,18 @@ using Newtonsoft.Json;
using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Api.Models.Tv;
using PlexRequests.Core.Models;
using PlexRequests.Core.Queue;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store.Models;
using PlexRequests.Store.Models.Plex;
using PlexRequests.Store.Repository;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
using Action = PlexRequests.Helpers.Analytics.Action;
using EpisodesModel = PlexRequests.Store.EpisodesModel;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
@ -72,10 +78,13 @@ namespace PlexRequests.UI.Modules
ISettingsService<PlexRequestSettings> prSettings, IAvailabilityChecker checker,
IRequestService request, ISonarrApi sonarrApi, ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<SickRageSettings> sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi,
INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService<HeadphonesSettings> hpService,
INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi,
ISettingsService<HeadphonesSettings> hpService,
ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi,
ISettingsService<PlexSettings> plexService, ISettingsService<AuthenticationSettings> auth, IRepository<UsersToNotify> u, ISettingsService<EmailNotificationSettings> email,
IIssueService issue, IAnalytics a, IRepository<RequestLimit> rl) : base("search", prSettings)
ISettingsService<PlexSettings> plexService, ISettingsService<AuthenticationSettings> auth,
IRepository<UsersToNotify> u, ISettingsService<EmailNotificationSettings> email,
IIssueService issue, IAnalytics a, IRepository<RequestLimit> rl, ITransientFaultQueue tfQueue, IRepository<PlexContent> content, ISecurityExtensions security)
: base("search", prSettings, security)
{
Auth = auth;
PlexService = plexService;
@ -103,7 +112,9 @@ namespace PlexRequests.UI.Modules
IssueService = issue;
Analytics = a;
RequestLimitRepo = rl;
FaultQueue = tfQueue;
TvApi = new TvMazeApi();
PlexContentRepository = content;
Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad();
@ -117,16 +128,16 @@ namespace PlexRequests.UI.Modules
Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies();
Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId);
Post["request/tv", true] = async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons);
Post["request/tv", true] =
async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons);
Post["request/tvEpisodes", true] = async (x, ct) => await RequestTvShow(0, "episode");
Post["request/album", true] = async (x, ct) => await RequestAlbum((string)Request.Form.albumId);
Post["/notifyuser", true] = async (x, ct) => await NotifyUser((bool)Request.Form.notify);
Get["/notifyuser", true] = async (x, ct) => await GetUserNotificationSettings();
Get["/seasons"] = x => GetSeasons();
Get["/episodes", true] = async (x, ct) => await GetEpisodes();
}
private IRepository<PlexContent> PlexContentRepository { get; }
private TvMazeApi TvApi { get; }
private IPlexApi PlexApi { get; }
private TheMovieDbApi MovieApi { get; }
@ -153,6 +164,7 @@ namespace PlexRequests.UI.Modules
private IRepository<UsersToNotify> UsersToNotifyRepo { get; }
private IIssueService IssueService { get; }
private IAnalytics Analytics { get; }
private ITransientFaultQueue FaultQueue { get; }
private IRepository<RequestLimit> RequestLimitRepo { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
@ -166,19 +178,22 @@ namespace PlexRequests.UI.Modules
private async Task<Response> UpcomingMovies()
{
Analytics.TrackEventAsync(Category.Search, Action.Movie, "Upcoming", Username, CookieHelper.GetAnalyticClientId(Cookies));
Analytics.TrackEventAsync(Category.Search, Action.Movie, "Upcoming", Username,
CookieHelper.GetAnalyticClientId(Cookies));
return await ProcessMovies(MovieSearchType.Upcoming, string.Empty);
}
private async Task<Response> CurrentlyPlayingMovies()
{
Analytics.TrackEventAsync(Category.Search, Action.Movie, "CurrentlyPlaying", Username, CookieHelper.GetAnalyticClientId(Cookies));
Analytics.TrackEventAsync(Category.Search, Action.Movie, "CurrentlyPlaying", Username,
CookieHelper.GetAnalyticClientId(Cookies));
return await ProcessMovies(MovieSearchType.CurrentlyPlaying, string.Empty);
}
private async Task<Response> SearchMovie(string searchTerm)
{
Analytics.TrackEventAsync(Category.Search, Action.Movie, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies));
Analytics.TrackEventAsync(Category.Search, Action.Movie, searchTerm, Username,
CookieHelper.GetAnalyticClientId(Cookies));
return await ProcessMovies(MovieSearchType.Search, searchTerm);
}
@ -191,24 +206,24 @@ namespace PlexRequests.UI.Modules
case MovieSearchType.Search:
var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false);
apiMovies = movies.Select(x =>
new MovieResult
{
Adult = x.Adult,
BackdropPath = x.BackdropPath,
GenreIds = x.GenreIds,
Id = x.Id,
OriginalLanguage = x.OriginalLanguage,
OriginalTitle = x.OriginalTitle,
Overview = x.Overview,
Popularity = x.Popularity,
PosterPath = x.PosterPath,
ReleaseDate = x.ReleaseDate,
Title = x.Title,
Video = x.Video,
VoteAverage = x.VoteAverage,
VoteCount = x.VoteCount
})
.ToList();
new MovieResult
{
Adult = x.Adult,
BackdropPath = x.BackdropPath,
GenreIds = x.GenreIds,
Id = x.Id,
OriginalLanguage = x.OriginalLanguage,
OriginalTitle = x.OriginalTitle,
Overview = x.Overview,
Popularity = x.Popularity,
PosterPath = x.PosterPath,
ReleaseDate = x.ReleaseDate,
Title = x.Title,
Video = x.Video,
VoteAverage = x.VoteAverage,
VoteCount = x.VoteCount
})
.ToList();
break;
case MovieSearchType.CurrentlyPlaying:
apiMovies = await MovieApi.GetCurrentPlayingMovies();
@ -229,7 +244,8 @@ namespace PlexRequests.UI.Modules
var cpCached = CpCacher.QueuedIds();
var plexMovies = Checker.GetPlexMovies();
var content = PlexContentRepository.GetAll();
var plexMovies = Checker.GetPlexMovies(content);
var settings = await PrService.GetSettingsAsync();
var viewMovies = new List<SearchMovieViewModel>();
var counter = 0;
@ -238,9 +254,10 @@ namespace PlexRequests.UI.Modules
var imdbId = string.Empty;
if (counter <= 5) // Let's only do it for the first 5 items
{
var movieInfoTask = await MovieApi.GetMovieInformation(movie.Id).ConfigureAwait(false); // TODO needs to be careful about this, it's adding extra time to search...
// https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2
imdbId = movieInfoTask.ImdbId;
var movieInfoTask = await MovieApi.GetMovieInformation(movie.Id).ConfigureAwait(false);
// TODO needs to be careful about this, it's adding extra time to search...
// https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2
imdbId = movieInfoTask?.ImdbId;
counter++;
}
@ -261,8 +278,9 @@ namespace PlexRequests.UI.Modules
VoteAverage = movie.VoteAverage,
VoteCount = movie.VoteCount
};
var canSee = CanUserSeeThisRequest(viewMovie.Id, settings.UsersCanViewOnlyOwnRequests, dbMovies);
var plexMovie = Checker.GetMovie(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString(), imdbId);
var canSee = CanUserSeeThisRequest(viewMovie.Id, Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests), dbMovies);
var plexMovie = Checker.GetMovie(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString(),
imdbId);
if (plexMovie != null)
{
viewMovie.Available = true;
@ -287,7 +305,8 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(viewMovies);
}
private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests, Dictionary<int, RequestedModel> moviesInDb)
private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests,
Dictionary<int, RequestedModel> moviesInDb)
{
if (usersCanViewOnlyOwnRequests)
{
@ -301,7 +320,8 @@ namespace PlexRequests.UI.Modules
private async Task<Response> SearchTvShow(string searchTerm)
{
Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies));
Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username,
CookieHelper.GetAnalyticClientId(Cookies));
var plexSettings = await PlexService.GetSettingsAsync();
var prSettings = await PrService.GetSettingsAsync();
var providerId = string.Empty;
@ -324,7 +344,8 @@ namespace PlexRequests.UI.Modules
var sonarrCached = SonarrCacher.QueuedIds();
var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays
var plexTvShows = Checker.GetPlexTvShows();
var content = PlexContentRepository.GetAll();
var plexTvShows = Checker.GetPlexTvShows(content);
var viewTv = new List<SearchTvShowViewModel>();
foreach (var t in apiTv)
@ -359,7 +380,8 @@ namespace PlexRequests.UI.Modules
providerId = viewT.Id.ToString();
}
var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), providerId);
var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4),
providerId);
if (plexShow != null)
{
viewT.Available = true;
@ -376,7 +398,8 @@ namespace PlexRequests.UI.Modules
viewT.Episodes = dbt.Episodes.ToList();
viewT.Approved = dbt.Approved;
}
if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid)) // compare to the sonarr/sickrage db
if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid))
// compare to the sonarr/sickrage db
{
viewT.Requested = true;
}
@ -390,7 +413,8 @@ namespace PlexRequests.UI.Modules
private async Task<Response> SearchAlbum(string searchTerm)
{
Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies));
Analytics.TrackEventAsync(Category.Search, Action.Album, searchTerm, Username,
CookieHelper.GetAnalyticClientId(Cookies));
var apiAlbums = new List<Release>();
await Task.Run(() => MusicBrainzApi.SearchAlbum(searchTerm)).ContinueWith((t) =>
{
@ -402,7 +426,8 @@ namespace PlexRequests.UI.Modules
var dbAlbum = allResults.ToDictionary(x => x.MusicBrainzId);
var plexAlbums = Checker.GetPlexAlbums();
var content = PlexContentRepository.GetAll();
var plexAlbums = Checker.GetPlexAlbums(content);
var viewAlbum = new List<SearchMusicViewModel>();
foreach (var a in apiAlbums)
@ -444,10 +469,10 @@ namespace PlexRequests.UI.Modules
private async Task<Response> RequestMovie(int movieId)
{
if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser))
if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMovie))
{
return
Response.AsJson(new JsonResponseModel()
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = "Sorry, you do not have the correct permissions to request a movie!"
@ -456,12 +481,28 @@ namespace PlexRequests.UI.Modules
var settings = await PrService.GetSettingsAsync();
if (!await CheckRequestLimit(settings, RequestType.Movie))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "You have reached your weekly request limit for Movies! Please contact your admin." });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = "You have reached your weekly request limit for Movies! Please contact your admin."
});
}
Analytics.TrackEventAsync(Category.Search, Action.Request, "Movie", Username, CookieHelper.GetAnalyticClientId(Cookies));
Analytics.TrackEventAsync(Category.Search, Action.Request, "Movie", Username,
CookieHelper.GetAnalyticClientId(Cookies));
var movieInfo = await MovieApi.GetMovieInformation(movieId);
var fullMovieName = $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}";
if (movieInfo == null)
{
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = "There was an issue adding this movie!"
});
}
var fullMovieName =
$"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}";
var existingRequest = await RequestService.CheckRequestAsync(movieId);
if (existingRequest != null)
@ -473,21 +514,41 @@ namespace PlexRequests.UI.Modules
await RequestService.UpdateRequestAsync(existingRequest);
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}" : $"{fullMovieName} {Resources.UI.Search_AlreadyRequested}" });
return
Response.AsJson(new JsonResponseModel
{
Result = true,
Message =
Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests)
? $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"
: $"{fullMovieName} {Resources.UI.Search_AlreadyRequested}"
});
}
try
{
var movies = Checker.GetPlexMovies();
var content = PlexContentRepository.GetAll();
var movies = Checker.GetPlexMovies(content);
if (Checker.IsMovieAvailable(movies.ToArray(), movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString()))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullMovieName} is already in Plex!" });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = $"{fullMovieName} is already in Plex!"
});
}
}
catch (Exception e)
{
Log.Error(e);
return Response.AsJson(new JsonResponseModel { Result = false, Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullMovieName) });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullMovieName)
});
}
//#endif
@ -507,41 +568,58 @@ namespace PlexRequests.UI.Modules
Issues = IssueState.None,
};
if (ShouldAutoApprove(RequestType.Movie, settings))
{
var cpSettings = await CpService.GetSettingsAsync();
model.Approved = true;
if (cpSettings.Enabled)
{
Log.Info("Adding movie to CP (No approval required)");
var result = CouchPotatoApi.AddMovie(model.ImdbId, cpSettings.ApiKey, model.Title,
cpSettings.FullUri, cpSettings.ProfileId);
Log.Debug("Adding movie to CP result {0}", result);
if (result)
{
return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
}
return Response.AsJson(new JsonResponseModel
{
Result = false,
Message = Resources.UI.Search_CouchPotatoError
});
}
model.Approved = true;
return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
}
try
{
if (ShouldAutoApprove(RequestType.Movie, settings, Username))
{
var cpSettings = await CpService.GetSettingsAsync();
model.Approved = true;
if (cpSettings.Enabled)
{
Log.Info("Adding movie to CP (No approval required)");
var result = CouchPotatoApi.AddMovie(model.ImdbId, cpSettings.ApiKey, model.Title,
cpSettings.FullUri, cpSettings.ProfileId);
Log.Debug("Adding movie to CP result {0}", result);
if (result)
{
return
await
AddRequest(model, settings,
$"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
}
return Response.AsJson(new JsonResponseModel
{
Result = false,
Message = Resources.UI.Search_CouchPotatoError
});
}
model.Approved = true;
return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
}
return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}");
}
catch (Exception e)
{
Log.Fatal(e);
await FaultQueue.QueueItemAsync(model, movieInfo.Id.ToString(), RequestType.Movie, FaultType.RequestFault, e.Message);
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_CouchPotatoError });
await NotificationService.Publish(new NotificationModel
{
DateTime = DateTime.Now,
User = Username,
RequestType = RequestType.Movie,
Title = model.Title,
NotificationType = NotificationType.ItemAddedToFaultQueue
});
return Response.AsJson(new JsonResponseModel
{
Result = true,
Message = $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"
});
}
}
@ -553,7 +631,7 @@ namespace PlexRequests.UI.Modules
/// <returns></returns>
private async Task<Response> RequestTvShow(int showId, string seasons)
{
if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser))
if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestTvShow))
{
return
Response.AsJson(new JsonResponseModel()
@ -575,9 +653,15 @@ namespace PlexRequests.UI.Modules
var settings = await PrService.GetSettingsAsync();
if (!await CheckRequestLimit(settings, RequestType.TvShow))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_WeeklyRequestLimitTVShow });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = Resources.UI.Search_WeeklyRequestLimitTVShow
});
}
Analytics.TrackEventAsync(Category.Search, Action.Request, "TvShow", Username, CookieHelper.GetAnalyticClientId(Cookies));
Analytics.TrackEventAsync(Category.Search, Action.Request, "TvShow", Username,
CookieHelper.GetAnalyticClientId(Cookies));
var sonarrSettings = SonarrService.GetSettingsAsync();
@ -589,7 +673,13 @@ namespace PlexRequests.UI.Modules
var s = await sonarrSettings;
if (!s.Enabled)
{
return Response.AsJson(new JsonResponseModel { Message = "This is currently only supported with Sonarr, Please enable Sonarr for this feature", Result = false });
return
Response.AsJson(new JsonResponseModel
{
Message =
"This is currently only supported with Sonarr, Please enable Sonarr for this feature",
Result = false
});
}
}
@ -598,14 +688,8 @@ namespace PlexRequests.UI.Modules
DateTime.TryParse(showInfo.premiered, out firstAir);
string fullShowName = $"{showInfo.name} ({firstAir.Year})";
if (showInfo.externals?.thetvdb == null)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Our TV Provider (TVMaze) doesn't have a TheTVDBId for this TV Show :( We cannot add the TV Show automatically sorry! Please report this problem to the server admin so he/she can sort it out!" });
}
var model = new RequestedModel
{
ProviderId = showInfo.externals?.thetvdb ?? 0,
Type = RequestType.TvShow,
Overview = showInfo.summary.RemoveHtml(),
PosterPath = showInfo.image?.medium,
@ -621,7 +705,6 @@ namespace PlexRequests.UI.Modules
TvDbId = showId.ToString()
};
var seasonsList = new List<int>();
switch (seasons)
{
@ -641,9 +724,14 @@ namespace PlexRequests.UI.Modules
foreach (var ep in episodeModel?.Episodes ?? new Models.EpisodesModel[0])
{
model.Episodes.Add(new EpisodesModel { EpisodeNumber = ep.EpisodeNumber, SeasonNumber = ep.SeasonNumber });
model.Episodes.Add(new EpisodesModel
{
EpisodeNumber = ep.EpisodeNumber,
SeasonNumber = ep.SeasonNumber
});
}
Analytics.TrackEventAsync(Category.Requests, Action.TvShow, $"Episode request for {model.Title}", Username, CookieHelper.GetAnalyticClientId(Cookies));
Analytics.TrackEventAsync(Category.Requests, Action.TvShow, $"Episode request for {model.Title}",
Username, CookieHelper.GetAnalyticClientId(Cookies));
break;
default:
model.SeasonsRequested = seasons;
@ -690,7 +778,12 @@ namespace PlexRequests.UI.Modules
else
{
// We no episodes to approve
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}"
});
}
}
else if (model.SeasonList.Except(existingRequest.SeasonList).Any())
@ -706,7 +799,9 @@ namespace PlexRequests.UI.Modules
try
{
var shows = Checker.GetPlexTvShows();
var content = PlexContentRepository.GetAll();
var shows = Checker.GetPlexTvShows(content);
var providerId = string.Empty;
var plexSettings = await PlexService.GetSettingsAsync();
if (plexSettings.AdvancedSearch)
@ -719,9 +814,19 @@ namespace PlexRequests.UI.Modules
var cachedEpisodes = cachedEpisodesTask.ToList();
foreach (var d in difference) // difference is from an existing request
{
if (cachedEpisodes.Any(x => x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber && x.ProviderId == providerId))
if (
cachedEpisodes.Any(
x =>
x.SeasonNumber == d.SeasonNumber && x.EpisodeNumber == d.EpisodeNumber &&
x.ProviderId == providerId))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}" });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message =
$"{fullShowName} {d.SeasonNumber} - {d.EpisodeNumber} {Resources.UI.Search_AlreadyInPlex}"
});
}
}
@ -737,73 +842,142 @@ namespace PlexRequests.UI.Modules
var result = Checker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, s.EpisodeNumber);
if (result)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}"
});
}
}
}
else if (Checker.IsTvShowAvailable(shows.ToArray(), showInfo.name, showInfo.premiered?.Substring(0, 4), providerId, model.SeasonList))
else if (Checker.IsTvShowAvailable(shows.ToArray(), showInfo.name, showInfo.premiered?.Substring(0, 4),
providerId, model.SeasonList))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}"
});
}
}
}
catch (Exception)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName) });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = string.Format(Resources.UI.Search_CouldNotCheckPlex, fullShowName)
});
}
if (ShouldAutoApprove(RequestType.TvShow, settings))
if (showInfo.externals?.thetvdb == null)
{
model.Approved = true;
var s = await sonarrSettings;
var sender = new TvSenderOld(SonarrApi, SickrageApi); // TODO put back
if (s.Enabled)
await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.MissingInformation, "We do not have a TheTVDBId from TVMaze");
await NotificationService.Publish(new NotificationModel
{
var result = await sender.SendToSonarr(s, model);
if (!string.IsNullOrEmpty(result?.title))
DateTime = DateTime.Now,
User = Username,
RequestType = RequestType.TvShow,
Title = model.Title,
NotificationType = NotificationType.ItemAddedToFaultQueue
});
return Response.AsJson(new JsonResponseModel
{
Result = true,
Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"
});
}
model.ProviderId = showInfo.externals?.thetvdb ?? 0;
try
{
if (ShouldAutoApprove(RequestType.TvShow, settings, Username))
{
model.Approved = true;
var s = await sonarrSettings;
var sender = new TvSenderOld(SonarrApi, SickrageApi); // TODO put back
if (s.Enabled)
{
if (existingRequest != null)
var result = await sender.SendToSonarr(s, model);
if (!string.IsNullOrEmpty(result?.title))
{
return await UpdateRequest(model, settings,
if (existingRequest != null)
{
return await UpdateRequest(model, settings,
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return
await
AddRequest(model, settings,
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
Log.Debug("Error with sending to sonarr.");
return
Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List<string>()));
}
Log.Debug("Error with sending to sonarr.");
return Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List<string>()));
}
var srSettings = SickRageService.GetSettings();
if (srSettings.Enabled)
{
var result = sender.SendToSickRage(srSettings, model);
if (result?.result == "success")
var srSettings = SickRageService.GetSettings();
if (srSettings.Enabled)
{
var result = sender.SendToSickRage(srSettings, model);
if (result?.result == "success")
{
return await AddRequest(model, settings,
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = result?.message ?? Resources.UI.Search_SickrageError
});
}
if (!srSettings.Enabled && !s.Enabled)
{
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.message ?? Resources.UI.Search_SickrageError });
return
Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp });
}
if (!srSettings.Enabled && !s.Enabled)
{
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp });
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
catch (Exception e)
{
await FaultQueue.QueueItemAsync(model, showInfo.id.ToString(), RequestType.TvShow, FaultType.RequestFault, e.Message);
await NotificationService.Publish(new NotificationModel
{
DateTime = DateTime.Now,
User = Username,
RequestType = RequestType.TvShow,
Title = model.Title,
NotificationType = NotificationType.ItemAddedToFaultQueue
});
Log.Error(e);
return
Response.AsJson(new JsonResponseModel
{
Result = true,
Message = $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"
});
}
return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
private async Task<Response> AddUserToRequest(RequestedModel existingRequest, PlexRequestSettings settings, string fullShowName, bool episodeReq = false)
private async Task<Response> AddUserToRequest(RequestedModel existingRequest, PlexRequestSettings settings,
string fullShowName, bool episodeReq = false)
{
// check if the current user is already marked as a requester for this show, if not, add them
if (!existingRequest.UserHasRequested(Username))
{
existingRequest.RequestedUsers.Add(Username);
}
if (settings.UsersCanViewOnlyOwnRequests || episodeReq)
if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) || episodeReq)
{
return
await
@ -811,20 +985,20 @@ namespace PlexRequests.UI.Modules
$"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}");
}
return await UpdateRequest(existingRequest, settings, $"{fullShowName} {Resources.UI.Search_AlreadyRequested}");
return
await UpdateRequest(existingRequest, settings, $"{fullShowName} {Resources.UI.Search_AlreadyRequested}");
}
private bool ShouldSendNotification(RequestType type, PlexRequestSettings prSettings)
{
var sendNotification = ShouldAutoApprove(type, prSettings) ? !prSettings.IgnoreNotifyForAutoApprovedRequests : true;
var claims = Context.CurrentUser?.Claims;
if (claims != null)
var sendNotification = ShouldAutoApprove(type, prSettings, Username)
? !prSettings.IgnoreNotifyForAutoApprovedRequests
: true;
if (IsAdmin)
{
var enumerable = claims as string[] ?? claims.ToArray();
if (enumerable.Contains(UserClaims.Admin) || enumerable.Contains(UserClaims.PowerUser))
{
sendNotification = false; // Don't bother sending a notification if the user is an admin
}
sendNotification = false; // Don't bother sending a notification if the user is an admin
}
return sendNotification;
}
@ -832,12 +1006,28 @@ namespace PlexRequests.UI.Modules
private async Task<Response> RequestAlbum(string releaseId)
{
if (Security.HasPermissions(User, Permissions.ReadOnlyUser) || !Security.HasPermissions(User, Permissions.RequestMusic))
{
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = "Sorry, you do not have the correct permissions to request music!"
});
}
var settings = await PrService.GetSettingsAsync();
if (!await CheckRequestLimit(settings, RequestType.Album))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_WeeklyRequestLimitAlbums });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = Resources.UI.Search_WeeklyRequestLimitAlbums
});
}
Analytics.TrackEventAsync(Category.Search, Action.Request, "Album", Username, CookieHelper.GetAnalyticClientId(Cookies));
Analytics.TrackEventAsync(Category.Search, Action.Request, "Album", Username,
CookieHelper.GetAnalyticClientId(Cookies));
var existingRequest = await RequestService.CheckRequestAsync(releaseId);
if (existingRequest != null)
@ -847,7 +1037,15 @@ namespace PlexRequests.UI.Modules
existingRequest.RequestedUsers.Add(Username);
await RequestService.UpdateRequestAsync(existingRequest);
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{existingRequest.Title} {Resources.UI.Search_SuccessfullyAdded}" : $"{existingRequest.Title} {Resources.UI.Search_AlreadyRequested}" });
return
Response.AsJson(new JsonResponseModel
{
Result = true,
Message =
Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests)
? $"{existingRequest.Title} {Resources.UI.Search_SuccessfullyAdded}"
: $"{existingRequest.Title} {Resources.UI.Search_AlreadyRequested}"
});
}
var albumInfo = MusicBrainzApi.GetAlbum(releaseId);
@ -857,11 +1055,19 @@ namespace PlexRequests.UI.Modules
var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist;
if (artist == null)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_MusicBrainzError });
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = Resources.UI.Search_MusicBrainzError
});
}
var albums = Checker.GetPlexAlbums();
var alreadyInPlex = Checker.IsAlbumAvailable(albums.ToArray(), albumInfo.title, release.ToString("yyyy"), artist.name);
var content = PlexContentRepository.GetAll();
var albums = Checker.GetPlexAlbums(content);
var alreadyInPlex = Checker.IsAlbumAvailable(albums.ToArray(), albumInfo.title, release.ToString("yyyy"),
artist.name);
if (alreadyInPlex)
{
@ -891,28 +1097,46 @@ namespace PlexRequests.UI.Modules
ArtistId = artist.id
};
if (ShouldAutoApprove(RequestType.Album, settings))
try
{
model.Approved = true;
var hpSettings = HeadphonesService.GetSettings();
if (!hpSettings.Enabled)
if (ShouldAutoApprove(RequestType.Album, settings, Username))
{
await RequestService.AddRequestAsync(model);
return
Response.AsJson(new JsonResponseModel
{
Result = true,
Message = $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"
});
model.Approved = true;
var hpSettings = HeadphonesService.GetSettings();
if (!hpSettings.Enabled)
{
await RequestService.AddRequestAsync(model);
return
Response.AsJson(new JsonResponseModel
{
Result = true,
Message = $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}"
});
}
var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService);
await sender.AddAlbum(model);
return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}");
}
var sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService);
await sender.AddAlbum(model);
return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}");
}
catch (Exception e)
{
Log.Error(e);
await FaultQueue.QueueItemAsync(model, albumInfo.id, RequestType.Album, FaultType.RequestFault, e.Message);
return await AddRequest(model, settings, $"{model.Title} {Resources.UI.Search_SuccessfullyAdded}");
await NotificationService.Publish(new NotificationModel
{
DateTime = DateTime.Now,
User = Username,
RequestType = RequestType.Album,
Title = model.Title,
NotificationType = NotificationType.ItemAddedToFaultQueue
});
throw;
}
}
private string GetMusicBrainzCoverArt(string id)
@ -928,85 +1152,7 @@ namespace PlexRequests.UI.Modules
return img;
}
private bool ShouldAutoApprove(RequestType requestType, PlexRequestSettings prSettings)
{
// if the user is an admin or they are whitelisted, they go ahead and allow auto-approval
if (IsAdmin || prSettings.ApprovalWhiteList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase))) return true;
// check by request type if the category requires approval or not
switch (requestType)
{
case RequestType.Movie:
return !prSettings.RequireMovieApproval;
case RequestType.TvShow:
return !prSettings.RequireTvShowApproval;
case RequestType.Album:
return !prSettings.RequireMusicApproval;
default:
return false;
}
}
private async Task<Response> NotifyUser(bool notify)
{
Analytics.TrackEventAsync(Category.Search, Action.Save, "NotifyUser", Username, CookieHelper.GetAnalyticClientId(Cookies), notify ? 1 : 0);
var authSettings = await Auth.GetSettingsAsync();
var auth = authSettings.UserAuthentication;
var emailSettings = await EmailNotificationSettings.GetSettingsAsync();
var email = emailSettings.EnableUserEmailNotifications;
if (!auth)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_ErrorPlexAccountOnly });
}
if (!email)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_ErrorNotEnabled });
}
var username = Username;
var originalList = await UsersToNotifyRepo.GetAllAsync();
if (!notify)
{
if (originalList == null)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_NotificationError });
}
var userToRemove = originalList.FirstOrDefault(x => x.Username == username);
if (userToRemove != null)
{
await UsersToNotifyRepo.DeleteAsync(userToRemove);
}
return Response.AsJson(new JsonResponseModel { Result = true });
}
if (originalList == null)
{
var userModel = new UsersToNotify { Username = username };
var insertResult = await UsersToNotifyRepo.InsertAsync(userModel);
return Response.AsJson(insertResult != -1 ? new JsonResponseModel { Result = true } : new JsonResponseModel { Result = false, Message = Resources.UI.Common_CouldNotSave });
}
var existingUser = originalList.FirstOrDefault(x => x.Username == username);
if (existingUser != null)
{
return Response.AsJson(new JsonResponseModel { Result = true }); // It's already enabled
}
else
{
var userModel = new UsersToNotify { Username = username };
var insertResult = await UsersToNotifyRepo.InsertAsync(userModel);
return Response.AsJson(insertResult != -1 ? new JsonResponseModel { Result = true } : new JsonResponseModel { Result = false, Message = Resources.UI.Common_CouldNotSave });
}
}
private async Task<Response> GetUserNotificationSettings()
{
var all = await UsersToNotifyRepo.GetAllAsync();
var retVal = all.FirstOrDefault(x => x.Username == Username);
return Response.AsJson(retVal != null);
}
private Response GetSeasons()
{
var seriesId = (int)Request.Query.tvId;
@ -1088,7 +1234,7 @@ namespace PlexRequests.UI.Modules
if (IsAdmin)
return true;
if (s.ApprovalWhiteList.Contains(Username))
if (Security.HasPermissions(User, Permissions.BypassRequestLimit))
return true;
var requestLimit = GetRequestLimitForType(type, s);
@ -1226,5 +1372,25 @@ namespace PlexRequests.UI.Modules
var diff = model.Episodes.Except(available);
return diff;
}
public bool ShouldAutoApprove(RequestType requestType, PlexRequestSettings prSettings, string username)
{
var admin = Security.HasPermissions(Context.CurrentUser, Permissions.Administrator);
// if the user is an admin, 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;
}
}
}
}

View file

@ -32,29 +32,31 @@ using System.Threading.Tasks;
using Nancy;
using Nancy.Extensions;
using Nancy.Linker;
using Nancy.Responses.Negotiation;
using NLog;
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.Analytics;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Models;
using PlexRequests.UI.Authentication;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.UI.Modules
{
public class UserLoginModule : BaseModule
{
public UserLoginModule(ISettingsService<AuthenticationSettings> auth, IPlexApi api, ISettingsService<PlexSettings> plexSettings, ISettingsService<PlexRequestSettings> pr,
ISettingsService<LandingPageSettings> lp, IAnalytics a, IResourceLinker linker, IRepository<UserLogins> userLogins) : base("userlogin", pr)
ISettingsService<LandingPageSettings> lp, IAnalytics a, IResourceLinker linker, IRepository<UserLogins> userLogins, IPlexUserRepository plexUsers, ICustomUserMapper custom,
ISecurityExtensions security, ISettingsService<UserManagementSettings> userManagementSettings)
: base("userlogin", pr, security)
{
AuthService = auth;
LandingPageSettings = lp;
@ -63,9 +65,40 @@ namespace PlexRequests.UI.Modules
PlexSettings = plexSettings;
Linker = linker;
UserLogins = userLogins;
PlexUserRepository = plexUsers;
CustomUserMapper = custom;
UserManagementSettings = userManagementSettings;
Get["UserLoginIndex", "/", true] = async (x, ct) =>
{
if (Request.Query["landing"] == null)
{
var s = await LandingPageSettings.GetSettingsAsync();
if (s.Enabled)
{
if (s.BeforeLogin) // Before login
{
if (string.IsNullOrEmpty(Username))
{
// They are not logged in
return
Context.GetRedirect(Linker.BuildRelativeUri(Context, "LandingPageIndex").ToString());
}
return Context.GetRedirect(Linker.BuildRelativeUri(Context, "SearchIndex").ToString());
}
// After login
if (string.IsNullOrEmpty(Username))
{
// Not logged in yet
return Context.GetRedirect(Linker.BuildRelativeUri(Context, "UserLoginIndex").ToString() + "?landing");
}
// Send them to landing
var landingUrl = Linker.BuildRelativeUri(Context, "LandingPageIndex").ToString();
return Context.GetRedirect(landingUrl);
}
}
if (!string.IsNullOrEmpty(Username) || IsAdmin)
{
var url = Linker.BuildRelativeUri(Context, "SearchIndex").ToString();
@ -86,12 +119,16 @@ namespace PlexRequests.UI.Modules
private IResourceLinker Linker { get; }
private IAnalytics Analytics { get; }
private IRepository<UserLogins> UserLogins { get; }
private IPlexUserRepository PlexUserRepository { get; }
private ICustomUserMapper CustomUserMapper { get; }
private ISettingsService<UserManagementSettings> UserManagementSettings {get;}
private static Logger Log = LogManager.GetCurrentClassLogger();
private async Task<Response> LoginUser()
{
var userId = string.Empty;
var loginGuid = Guid.Empty;
var dateTimeOffset = Request.Form.DateTimeOffset;
var username = Request.Form.username.Value;
Log.Debug("Username \"{0}\" attempting to login", username);
@ -99,10 +136,11 @@ namespace PlexRequests.UI.Modules
{
Session["TempMessage"] = Resources.UI.UserLogin_IncorrectUserPass;
var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex");
return Response.AsRedirect(uri.ToString()); // TODO Check this
return Response.AsRedirect(uri.ToString());
}
var authenticated = false;
var isOwner = false;
var settings = await AuthService.GetSettingsAsync();
var plexSettings = await PlexSettings.GetSettingsAsync();
@ -112,7 +150,7 @@ namespace PlexRequests.UI.Modules
Log.Debug("User is in denied list, not allowing them to authenticate");
Session["TempMessage"] = Resources.UI.UserLogin_IncorrectUserPass;
var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex");
return Response.AsRedirect(uri.ToString()); // TODO Check this
return Response.AsRedirect(uri.ToString());
}
var password = string.Empty;
@ -122,6 +160,9 @@ namespace PlexRequests.UI.Modules
password = Request.Form.password.Value;
}
var localUsers = await CustomUserMapper.GetUsersAsync();
var plexLocalUsers = await PlexUserRepository.GetAllAsync();
if (settings.UserAuthentication && settings.UsePassword) // Authenticate with Plex
{
@ -134,6 +175,7 @@ namespace PlexRequests.UI.Modules
{
Log.Debug("User is the account owner");
authenticated = true;
isOwner = true;
}
else
{
@ -155,6 +197,7 @@ namespace PlexRequests.UI.Modules
{
Log.Debug("User is the account owner");
authenticated = true;
isOwner = true;
userId = GetOwnerId(plexSettings.PlexAuthToken, username);
}
Log.Debug("Friends list result = {0}", authenticated);
@ -172,13 +215,53 @@ namespace PlexRequests.UI.Modules
// Add to the session (Used in the BaseModules)
Session[SessionKeys.UsernameKey] = (string)username;
Session[SessionKeys.ClientDateTimeOffsetKey] = (int)dateTimeOffset;
var plexLocal = plexLocalUsers.FirstOrDefault(x => x.Username == username);
if (plexLocal != null)
{
loginGuid = Guid.Parse(plexLocal.LoginId);
}
var dbUser = localUsers.FirstOrDefault(x => x.UserName == username);
if (dbUser != null)
{
loginGuid = Guid.Parse(dbUser.UserGuid);
}
if(loginGuid == Guid.Empty && settings.UserAuthentication)
{
var defaultSettings = UserManagementSettings.GetSettings();
loginGuid = Guid.NewGuid();
var defaultPermissions = (Permissions)UserManagementHelper.GetPermissions(defaultSettings);
if (isOwner)
{
// If we are the owner, add the admin permission.
if (!defaultPermissions.HasFlag(Permissions.Administrator))
{
defaultPermissions += (int)Permissions.Administrator;
}
}
// Looks like we still don't have an entry, so this user does not exist
await PlexUserRepository.InsertAsync(new PlexUsers
{
PlexUserId = userId,
UserAlias = string.Empty,
Permissions = (int)defaultPermissions,
Features = UserManagementHelper.GetPermissions(defaultSettings),
Username = username,
EmailAddress = string.Empty, // We don't have it, we will get it on the next scheduled job run (in 30 mins)
LoginId = loginGuid.ToString()
});
}
}
if (!authenticated)
{
var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex");
Session["TempMessage"] = Resources.UI.UserLogin_IncorrectUserPass;
return Response.AsRedirect(uri.ToString()); // TODO Check this
return Response.AsRedirect(uri.ToString());
}
var landingSettings = await LandingPageSettings.GetSettingsAsync();
@ -188,11 +271,21 @@ namespace PlexRequests.UI.Modules
if (!landingSettings.BeforeLogin)
{
var uri = Linker.BuildRelativeUri(Context, "LandingPageIndex");
if (loginGuid != Guid.Empty)
{
return CustomModuleExtensions.LoginAndRedirect(this, loginGuid, null, uri.ToString());
}
return Response.AsRedirect(uri.ToString());
}
}
var retVal = Linker.BuildRelativeUri(Context, "SearchIndex");
return Response.AsRedirect(retVal.ToString()); // TODO Check this
if (loginGuid != Guid.Empty)
{
return CustomModuleExtensions.LoginAndRedirect(this, loginGuid, null, retVal.ToString());
}
return Response.AsRedirect(retVal.ToString());
}

View file

@ -6,39 +6,50 @@ using System.Threading.Tasks;
using Nancy;
using Nancy.Extensions;
using Nancy.Responses.Negotiation;
using Nancy.Security;
using Newtonsoft.Json;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.Models;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Models;
using Action = PlexRequests.Helpers.Analytics.Action;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
namespace PlexRequests.UI.Modules
{
public class UserManagementModule : BaseModule
{
public UserManagementModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService<PlexSettings> plex, IRepository<UserLogins> userLogins) : base("usermanagement", pr)
public UserManagementModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService<PlexSettings> plex, IRepository<UserLogins> userLogins, IPlexUserRepository plexRepo
, ISecurityExtensions security, IRequestService req, IAnalytics ana) : base("usermanagement", pr, security)
{
#if !DEBUG
this.RequiresClaims(UserClaims.Admin);
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
#endif
UserMapper = m;
PlexApi = plexApi;
PlexSettings = plex;
UserLoginsRepo = userLogins;
PlexUsersRepository = plexRepo;
PlexRequestSettings = pr;
RequestService = req;
Analytics = ana;
Get["/"] = x => Load();
Get["/users", true] = async (x, ct) => await LoadUsers();
Post["/createuser"] = x => CreateUser();
Post["/createuser", true] = async (x, ct) => await CreateUser();
Get["/local/{id}"] = x => LocalDetails((Guid)x.id);
Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id);
Get["/claims"] = x => GetClaims();
Post["/updateuser"] = x => UpdateUser();
Get["/permissions"] = x => GetEnum<Permissions>();
Get["/features"] = x => GetEnum<Features>();
Post["/updateuser", true] = async (x, ct) => await UpdateUser();
Post["/deleteuser"] = x => DeleteUser();
}
@ -46,6 +57,10 @@ namespace PlexRequests.UI.Modules
private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private IRepository<UserLogins> UserLoginsRepo { get; }
private IPlexUserRepository PlexUsersRepository { get; }
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private IRequestService RequestService { get; }
private IAnalytics Analytics { get; }
private Negotiator Load()
{
@ -55,13 +70,14 @@ namespace PlexRequests.UI.Modules
private async Task<Response> LoadUsers()
{
var localUsers = await UserMapper.GetUsersAsync();
var plexDbUsers = await PlexUsersRepository.GetAllAsync();
var model = new List<UserManagementUsersViewModel>();
var usersDb = UserLoginsRepo.GetAll().ToList();
var userLogins = UserLoginsRepo.GetAll().ToList();
foreach (var user in localUsers)
{
var userDb = usersDb.FirstOrDefault(x => x.UserId == user.UserGuid);
var userDb = userLogins.FirstOrDefault(x => x.UserId == user.UserGuid);
model.Add(MapLocalUser(user, userDb?.LastLoggedIn ?? DateTime.MinValue));
}
@ -73,27 +89,36 @@ namespace PlexRequests.UI.Modules
foreach (var u in plexUsers.User)
{
var userDb = usersDb.FirstOrDefault(x => x.UserId == u.Id);
model.Add(new UserManagementUsersViewModel
var dbUser = plexDbUsers.FirstOrDefault(x => x.PlexUserId == u.Id);
var userDb = userLogins.FirstOrDefault(x => x.UserId == u.Id);
// We don't have the user in the database yet
if (dbUser == null)
{
Username = u.Username,
Type = UserType.PlexUser,
Id = u.Id,
Claims = "Requestor",
EmailAddress = u.Email,
PlexInfo = new UserManagementPlexInformation
{
Thumb = u.Thumb
},
LastLoggedIn = userDb?.LastLoggedIn ?? DateTime.MinValue,
});
model.Add(MapPlexUser(u, null, userDb?.LastLoggedIn ?? DateTime.MinValue));
}
else
{
// The Plex User is in the database
model.Add(MapPlexUser(u, dbUser, userDb?.LastLoggedIn ?? DateTime.MinValue));
}
}
// Also get the server admin
var account = PlexApi.GetAccount(plexSettings.PlexAuthToken);
if (account != null)
{
var dbUser = plexDbUsers.FirstOrDefault(x => x.PlexUserId == account.Id);
var userDb = userLogins.FirstOrDefault(x => x.UserId == account.Id);
model.Add(MapPlexAdmin(account, dbUser, userDb?.LastLoggedIn ?? DateTime.MinValue));
}
}
return Response.AsJson(model);
}
private Response CreateUser()
private async Task<Response> CreateUser()
{
Analytics.TrackEventAsync(Category.UserManagement, Action.Create, "Created User", Username, CookieHelper.GetAnalyticClientId(Cookies));
var body = Request.Body.AsString();
if (string.IsNullOrEmpty(body))
{
@ -106,11 +131,37 @@ namespace PlexRequests.UI.Modules
{
return Response.AsJson(new JsonResponseModel
{
Result = true,
Result = false,
Message = "Please enter in a valid Username and Password"
});
}
var user = UserMapper.CreateUser(model.Username, model.Password, model.Claims, new UserProperties { EmailAddress = model.EmailAddress });
var users = await UserMapper.GetUsersAsync();
if (users.Any(x => x.UserName.Equals(model.Username, StringComparison.CurrentCultureIgnoreCase)))
{
return Response.AsJson(new JsonResponseModel
{
Result = false,
Message = $"A user with the username '{model.Username}' already exists"
});
}
var featuresVal = 0;
var permissionsVal = 0;
foreach (var feature in model.Features)
{
var f = (int)EnumHelper<Features>.GetValueFromName(feature);
featuresVal += f;
}
foreach (var permission in model.Permissions)
{
var f = (int)EnumHelper<Permissions>.GetValueFromName(permission);
permissionsVal += f;
}
var user = UserMapper.CreateUser(model.Username, model.Password, permissionsVal, featuresVal, new UserProperties { EmailAddress = model.EmailAddress });
if (user.HasValue)
{
return Response.AsJson(MapLocalUser(UserMapper.GetUser(user.Value), DateTime.MinValue));
@ -119,8 +170,9 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not save user" });
}
private Response UpdateUser()
private async Task<Response> UpdateUser()
{
Analytics.TrackEventAsync(Category.UserManagement, Action.Update, "Updated User", Username, CookieHelper.GetAnalyticClientId(Cookies));
var body = Request.Body.AsString();
if (string.IsNullOrEmpty(body))
{
@ -138,33 +190,124 @@ namespace PlexRequests.UI.Modules
});
}
var claims = new List<string>();
var permissionsValue = model.Permissions.Where(c => c.Selected).Sum(c => c.Value);
var featuresValue = model.Features.Where(c => c.Selected).Sum(c => c.Value);
foreach (var c in model.Claims)
Guid outId;
Guid.TryParse(model.Id, out outId);
var localUser = UserMapper.GetUser(outId);
// Update Local User
if (localUser != null)
{
if (c.Selected)
{
claims.Add(c.Name);
}
localUser.Permissions = permissionsValue;
localUser.Features = featuresValue;
var currentProps = ByteConverterHelper.ReturnObject<UserProperties>(localUser.UserProperties);
// Let's check if the alias has changed, if so we need to change all the requests associated with this
await UpdateRequests(localUser.UserName, currentProps.UserAlias, model.Alias);
currentProps.UserAlias = model.Alias;
currentProps.EmailAddress = model.EmailAddress;
localUser.UserProperties = ByteConverterHelper.ReturnBytes(currentProps);
var user = UserMapper.EditUser(localUser);
var dbUser = UserLoginsRepo.GetAll().FirstOrDefault(x => x.UserId == user.UserGuid);
var retUser = MapLocalUser(user, dbUser?.LastLoggedIn ?? DateTime.MinValue);
return Response.AsJson(retUser);
}
var userFound = UserMapper.GetUser(new Guid(model.Id));
var plexSettings = await PlexSettings.GetSettingsAsync();
var plexDbUsers = await PlexUsersRepository.GetAllAsync();
var plexUsers = PlexApi.GetUsers(plexSettings.PlexAuthToken);
var plexDbUser = plexDbUsers.FirstOrDefault(x => x.PlexUserId == model.Id);
var plexUser = plexUsers.User.FirstOrDefault(x => x.Id == model.Id);
var userLogin = UserLoginsRepo.GetAll().FirstOrDefault(x => x.UserId == model.Id);
if (plexDbUser != null && plexUser != null)
{
// We have a user in the DB for this Plex Account
plexDbUser.Permissions = permissionsValue;
plexDbUser.Features = featuresValue;
userFound.Claims = ByteConverterHelper.ReturnBytes(claims.ToArray());
var currentProps = ByteConverterHelper.ReturnObject<UserProperties>(userFound.UserProperties);
currentProps.UserAlias = model.Alias;
currentProps.EmailAddress = model.EmailAddress;
await UpdateRequests(plexDbUser.Username, plexDbUser.UserAlias, model.Alias);
userFound.UserProperties = ByteConverterHelper.ReturnBytes(currentProps);
plexDbUser.UserAlias = model.Alias;
var user = UserMapper.EditUser(userFound);
var dbUser = UserLoginsRepo.GetAll().FirstOrDefault(x => x.UserId == user.UserGuid);
var retUser = MapLocalUser(user, dbUser?.LastLoggedIn ?? DateTime.MinValue);
return Response.AsJson(retUser);
await PlexUsersRepository.UpdateAsync(plexDbUser);
var retUser = MapPlexUser(plexUser, plexDbUser, userLogin?.LastLoggedIn ?? DateTime.MinValue);
return Response.AsJson(retUser);
}
// So it could actually be the admin
var account = PlexApi.GetAccount(plexSettings.PlexAuthToken);
if (plexDbUser != null && account != null)
{
// We have a user in the DB for this Plex Account
plexDbUser.Permissions = permissionsValue;
plexDbUser.Features = featuresValue;
await UpdateRequests(plexDbUser.Username, plexDbUser.UserAlias, model.Alias);
plexDbUser.UserAlias = model.Alias;
await PlexUsersRepository.UpdateAsync(plexDbUser);
var retUser = MapPlexAdmin(account, plexDbUser, userLogin?.LastLoggedIn ?? DateTime.MinValue);
return Response.AsJson(retUser);
}
// We have a Plex Account but he's not in the DB
if (plexUser != null)
{
var user = new PlexUsers
{
Permissions = permissionsValue,
Features = featuresValue,
UserAlias = model.Alias,
PlexUserId = plexUser.Id,
EmailAddress = plexUser.Email,
Username = plexUser.Username,
LoginId = Guid.NewGuid().ToString()
};
await PlexUsersRepository.InsertAsync(user);
var retUser = MapPlexUser(plexUser, user, userLogin?.LastLoggedIn ?? DateTime.MinValue);
return Response.AsJson(retUser);
}
return null; // We should never end up here.
}
private async Task UpdateRequests(string username, string oldAlias, string newAlias)
{
var newUsername = string.IsNullOrEmpty(newAlias) ? username : newAlias; // User the username if we are clearing the alias
var olderUsername = string.IsNullOrEmpty(oldAlias) ? username : oldAlias;
// Let's check if the alias has changed, if so we need to change all the requests associated with this
if (!olderUsername.Equals(newUsername, StringComparison.CurrentCultureIgnoreCase))
{
var requests = await RequestService.GetAllAsync();
// Update all requests
var requestsWithThisUser = requests.Where(x => x.RequestedUsers.Contains(olderUsername)).ToList();
foreach (var r in requestsWithThisUser)
{
r.RequestedUsers.Remove(olderUsername); // Remove old
r.RequestedUsers.Add(newUsername); // Add new
}
if (requestsWithThisUser.Any())
{
RequestService.BatchUpdate(requestsWithThisUser);
}
}
}
private Response DeleteUser()
{
Analytics.TrackEventAsync(Category.UserManagement, Action.Delete, "Deleted User", Username, CookieHelper.GetAnalyticClientId(Cookies));
var body = Request.Body.AsString();
if (string.IsNullOrEmpty(body))
{
@ -182,9 +325,9 @@ namespace PlexRequests.UI.Modules
});
}
UserMapper.DeleteUser(model.Id);
UserMapper.DeleteUser(model.Id);
return Response.AsJson(new JsonResponseModel {Result = true});
return Response.AsJson(new JsonResponseModel { Result = true });
}
private Response LocalDetails(Guid id)
@ -221,55 +364,144 @@ namespace PlexRequests.UI.Modules
/// Returns all claims for the users.
/// </summary>
/// <returns></returns>
private Response GetClaims()
private Response GetEnum<T>()
{
var retVal = new List<dynamic>();
var claims = UserMapper.GetAllClaims();
foreach (var c in claims)
var retVal = new List<CheckBox>();
foreach (var p in Enum.GetValues(typeof(T)))
{
retVal.Add(new { Name = c, Selected = false });
var perm = (T)p;
var displayValue = EnumHelper<T>.GetDisplayValue(perm);
retVal.Add(new CheckBox { Name = displayValue, Selected = false, Value = (int)p });
}
return Response.AsJson(retVal);
}
private UserManagementUsersViewModel MapLocalUser(UsersModel user, DateTime lastLoggedIn)
{
var claims = ByteConverterHelper.ReturnObject<string[]>(user.Claims);
var claimsString = string.Join(", ", claims);
var features = (Features)user.Features;
var permissions = (Permissions)user.Permissions;
var userProps = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties);
var m = new UserManagementUsersViewModel
{
Id = user.UserGuid,
Claims = claimsString,
PermissionsFormattedString = permissions == 0 ? "None" : permissions.ToString(),
FeaturesFormattedString = features.ToString(),
Username = user.UserName,
Type = UserType.LocalUser,
EmailAddress = userProps.EmailAddress,
Alias = userProps.UserAlias,
ClaimsArray = claims,
ClaimsItem = new List<UserManagementUpdateModel.ClaimsModel>(),
LastLoggedIn = lastLoggedIn
LastLoggedIn = lastLoggedIn,
};
// Add all of the current claims
foreach (var c in claims)
{
m.ClaimsItem.Add(new UserManagementUpdateModel.ClaimsModel { Name = c, Selected = true });
}
var allClaims = UserMapper.GetAllClaims();
m.Permissions.AddRange(GetPermissions(permissions));
m.Features.AddRange(GetFeatures(features));
// Get me the current claims that the user does not have
var missingClaims = allClaims.Except(claims);
// Add them into the view
foreach (var missingClaim in missingClaims)
{
m.ClaimsItem.Add(new UserManagementUpdateModel.ClaimsModel { Name = missingClaim, Selected = false });
}
return m;
}
private UserManagementUsersViewModel MapPlexUser(UserFriends plexInfo, PlexUsers dbUser, DateTime lastLoggedIn)
{
if (dbUser == null)
{
dbUser = new PlexUsers();
}
var features = (Features)dbUser?.Features;
var permissions = (Permissions)dbUser?.Permissions;
var m = new UserManagementUsersViewModel
{
Id = plexInfo.Id,
PermissionsFormattedString = permissions == 0 ? "None" : permissions.ToString(),
FeaturesFormattedString = features.ToString(),
Username = plexInfo.Username,
Type = UserType.PlexUser,
EmailAddress = plexInfo.Email,
Alias = dbUser?.UserAlias ?? string.Empty,
LastLoggedIn = lastLoggedIn,
PlexInfo = new UserManagementPlexInformation
{
Thumb = plexInfo.Thumb
},
};
m.Permissions.AddRange(GetPermissions(permissions));
m.Features.AddRange(GetFeatures(features));
return m;
}
private UserManagementUsersViewModel MapPlexAdmin(PlexAccount plexInfo, PlexUsers dbUser, DateTime lastLoggedIn)
{
if (dbUser == null)
{
dbUser = new PlexUsers();
}
var features = (Features)dbUser?.Features;
var permissions = (Permissions)dbUser?.Permissions;
var m = new UserManagementUsersViewModel
{
Id = plexInfo.Id,
PermissionsFormattedString = permissions == 0 ? "None" : permissions.ToString(),
FeaturesFormattedString = features.ToString(),
Username = plexInfo.Username,
Type = UserType.PlexUser,
EmailAddress = plexInfo.Email,
Alias = dbUser?.UserAlias ?? string.Empty,
LastLoggedIn = lastLoggedIn,
};
m.Permissions.AddRange(GetPermissions(permissions));
m.Features.AddRange(GetFeatures(features));
return m;
}
private List<CheckBox> GetPermissions(Permissions permissions)
{
var retVal = new List<CheckBox>();
// Add permissions
foreach (var p in Enum.GetValues(typeof(Permissions)))
{
var perm = (Permissions)p;
var displayValue = EnumHelper<Permissions>.GetDisplayValue(perm);
var pm = new CheckBox
{
Name = displayValue,
Selected = permissions.HasFlag(perm),
Value = (int)perm
};
retVal.Add(pm);
}
return retVal;
}
private List<CheckBox> GetFeatures(Features features)
{
var retVal = new List<CheckBox>();
// Add features
foreach (var p in Enum.GetValues(typeof(Features)))
{
var perm = (Features)p;
var displayValue = EnumHelper<Features>.GetDisplayValue(perm);
var pm = new CheckBox
{
Name = displayValue,
Selected = features.HasFlag(perm),
Value = (int)perm
};
retVal.Add(pm);
}
return retVal;
}
}
}

View file

@ -29,10 +29,8 @@ using System.Linq;
using System.Threading.Tasks;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Extensions;
using Nancy.ModelBinding;
using Nancy.Responses.Negotiation;
using Nancy.Validation;
using NLog;
using PlexRequests.Api.Interfaces;
@ -40,8 +38,11 @@ using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Authentication;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
using Action = PlexRequests.Helpers.Analytics.Action;
@ -50,7 +51,7 @@ namespace PlexRequests.UI.Modules
public class UserWizardModule : BaseModule
{
public UserWizardModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<PlexSettings> plex, IPlexApi plexApi,
ISettingsService<AuthenticationSettings> auth, ICustomUserMapper m, IAnalytics a) : base("wizard", pr)
ISettingsService<AuthenticationSettings> auth, ICustomUserMapper m, IAnalytics a, ISecurityExtensions security) : base("wizard", pr, security)
{
PlexSettings = plex;
PlexApi = plexApi;
@ -157,10 +158,7 @@ namespace PlexRequests.UI.Modules
currentSettings.SearchForMovies = form.SearchForMovies;
currentSettings.SearchForTvShows = form.SearchForTvShows;
currentSettings.SearchForMusic = form.SearchForMusic;
currentSettings.RequireMovieApproval = form.RequireMovieApproval;
currentSettings.RequireTvShowApproval = form.RequireTvShowApproval;
currentSettings.RequireMusicApproval = form.RequireMusicApproval;
var result = await PlexRequestSettings.SaveSettingsAsync(currentSettings);
if (result)
{
@ -185,7 +183,7 @@ namespace PlexRequests.UI.Modules
private async Task<Response> CreateUser()
{
var username = (string)Request.Form.Username;
var userId = Mapper.CreateAdmin(username, Request.Form.Password);
var userId = Mapper.CreateUser(username, Request.Form.Password, EnumHelper<Permissions>.All() - (int)Permissions.ReadOnlyUser, 0);
Analytics.TrackEventAsync(Category.Wizard, Action.Finish, "Finished the wizard", username, CookieHelper.GetAnalyticClientId(Cookies));
Session[SessionKeys.UsernameKey] = username;
@ -199,7 +197,7 @@ namespace PlexRequests.UI.Modules
var baseUrl = string.IsNullOrEmpty(settings.BaseUrl) ? string.Empty : $"/{settings.BaseUrl}";
return this.LoginAndRedirect((Guid)userId, fallbackRedirectUrl: $"{baseUrl}/search");
return CustomModuleExtensions.LoginAndRedirect(this,(Guid)userId, fallbackRedirectUrl: $"{baseUrl}/search");
}
}
}

View file

@ -25,18 +25,20 @@
// ************************************************************************/
#endregion
using Mono.Data.Sqlite;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Linker;
using Ninject.Modules;
using PlexRequests.Core;
using PlexRequests.Core.Migration;
using PlexRequests.Core.StatusChecker;
using PlexRequests.Core.Users;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification;
using PlexRequests.Store;
using ISecurityExtensions = PlexRequests.Core.ISecurityExtensions;
using SecurityExtensions = PlexRequests.Core.SecurityExtensions;
namespace PlexRequests.UI.NinjectModules
{
@ -56,6 +58,11 @@ namespace PlexRequests.UI.NinjectModules
Bind<INotificationService>().To<NotificationService>().InSingletonScope();
Bind<INotificationEngine>().To<NotificationEngine>();
Bind<IStatusChecker>().To<StatusChecker>();
Bind<ISecurityExtensions>().To<SecurityExtensions>();
Bind<IUserHelper>().To<UserHelper>();
}
}
}

View file

@ -46,6 +46,9 @@ namespace PlexRequests.UI.NinjectModules
Bind<IIssueService>().To<IssueJsonService>();
Bind<ISettingsRepository>().To<SettingsJsonRepository>();
Bind<IJobRecord>().To<JobRecord>();
Bind<IUserRepository>().To<UserRepository>();
Bind<IPlexUserRepository>().To<PlexUserRepository>();
}
}

View file

@ -25,7 +25,7 @@
// ************************************************************************/
#endregion
using Ninject.Modules;
using PlexRequests.Core.Queue;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs;
@ -46,11 +46,14 @@ namespace PlexRequests.UI.NinjectModules
Bind<ISonarrCacher>().To<SonarrCacher>();
Bind<ISickRageCacher>().To<SickRageCacher>();
Bind<IRecentlyAdded>().To<RecentlyAdded>();
Bind<IPlexContentCacher>().To<PlexContentCacher>();
Bind<IJobFactory>().To<CustomJobFactory>();
Bind<IAnalytics>().To<Analytics>();
Bind<ISchedulerFactory>().To<StdSchedulerFactory>();
Bind<IJobScheduler>().To<Scheduler>();
Bind<ITransientFaultQueue>().To<TransientFaultQueue>();
}
}
}

View file

@ -203,6 +203,9 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Authentication\CustomAuthenticationConfiguration.cs" />
<Compile Include="Authentication\CustomAuthenticationProvider.cs" />
<Compile Include="Authentication\CustomModuleExtensions.cs" />
<Compile Include="Bootstrapper.cs" />
<Compile Include="Helpers\BaseUrlHelper.cs" />
<Compile Include="Helpers\ContravariantBindingResolver.cs" />
@ -210,14 +213,10 @@
<Compile Include="Helpers\CustomHtmlHelper.cs" />
<Compile Include="Helpers\DebugRootPathProvider.cs" />
<Compile Include="Helpers\EmptyViewBase.cs" />
<Compile Include="Helpers\HeadphonesSender.cs" />
<Compile Include="Helpers\AngularViewBase.cs" />
<Compile Include="Helpers\HtmlSecurityHelper.cs" />
<Compile Include="Helpers\SecurityExtensions.cs" />
<Compile Include="Helpers\ServiceLocator.cs" />
<Compile Include="Helpers\Themes.cs" />
<Compile Include="Helpers\TvSender.cs" />
<Compile Include="Helpers\TvSenderOld.cs" />
<Compile Include="Helpers\ValidationHelper.cs" />
<Compile Include="Jobs\CustomJobFactory.cs" />
<Compile Include="Jobs\Scheduler.cs" />
@ -232,6 +231,7 @@
<Compile Include="Models\DatatablesModel.cs" />
<Compile Include="Models\EpisodeListViewModel.cs" />
<Compile Include="Models\EpisodeRequestModel.cs" />
<Compile Include="Models\FaultedRequestsViewModel.cs" />
<Compile Include="Models\IssuesDetailsViewModel.cs" />
<Compile Include="Models\IssuesViewMOdel.cs" />
<Compile Include="Models\JsonUpdateAvailableModel.cs" />
@ -244,6 +244,9 @@
<Compile Include="Models\SearchMovieViewModel.cs" />
<Compile Include="Models\UserManagement\DeleteUserViewModel.cs" />
<Compile Include="Models\UserManagement\UserUpdateViewModel.cs" />
<Compile Include="Modules\Admin\UserManagementSettingsModule.cs" />
<Compile Include="Modules\Admin\FaultQueueModule.cs" />
<Compile Include="Modules\Admin\SystemStatusModule.cs" />
<Compile Include="Modules\ApiDocsModule.cs" />
<Compile Include="Modules\ApiSettingsMetadataModule.cs" />
<Compile Include="Modules\ApiUserMetadataModule.cs" />
@ -257,8 +260,7 @@
<Compile Include="Modules\DonationLinkModule.cs" />
<Compile Include="Modules\IssuesModule.cs" />
<Compile Include="Modules\LandingPageModule.cs" />
<Compile Include="Modules\RequestsBetaModule.cs" />
<Compile Include="Modules\UpdateCheckerModule.cs" />
<Compile Include="Modules\LayoutModule.cs" />
<Compile Include="Modules\UserWizardModule.cs" />
<Compile Include="NinjectModules\ApiModule.cs" />
<Compile Include="NinjectModules\ConfigurationModule.cs" />
@ -287,6 +289,12 @@
<Content Include="Content\angular.min.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Content\Angular\angular-loading-spinner.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\Angular\angular-spinner.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\app\app.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@ -296,6 +304,18 @@
<Content Include="Content\app\requests\requestsService.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Content\app\userManagement\Directives\sidebar.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\app\userManagement\Directives\addUser.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\app\userManagement\Directives\table.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Content\app\userManagement\Directives\userManagementDirective.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\awesome-bootstrap-checkbox.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -323,8 +343,7 @@
<Compile Include="Models\PlexAuth.cs" />
<Compile Include="Models\RequestViewModel.cs" />
<Compile Include="Models\SearchTvShowViewModel.cs" />
<Compile Include="Models\SessionKeys.cs" />
<Compile Include="Modules\AdminModule.cs" />
<Compile Include="Modules\Admin\AdminModule.cs" />
<Compile Include="Modules\ApplicationTesterModule.cs" />
<Compile Include="Modules\BaseAuthModule.cs" />
<Compile Include="Modules\IndexModule.cs" />
@ -338,6 +357,9 @@
<Compile Include="Startup.cs" />
<Compile Include="Validators\PlexRequestsValidator.cs" />
<Compile Include="Modules\UserManagementModule.cs" />
<Content Include="Content\clipboard.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\datepicker.css">
<DependentUpon>datepicker.scss</DependentUpon>
</Content>
@ -351,6 +373,9 @@
<Content Include="Content\analytics.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\spin.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\wizard.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -660,7 +685,7 @@
<Content Include="Views\Admin\EmailNotifications.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Views\Admin\Status.cshtml">
<Content Include="Views\SystemStatus\Status.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Views\Admin\PushbulletNotifications.cshtml">
@ -681,7 +706,7 @@
<Content Include="Views\Admin\Headphones.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Views\Admin\_Sidebar.cshtml">
<Content Include="Views\Shared\Partial\_Sidebar.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Views\Admin\SlackNotifications.cshtml">
@ -726,6 +751,12 @@
<None Include="Views\Admin\NewsletterSettings.cshtml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Include="Views\FaultQueue\RequestFaultQueue.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Views\UserManagementSettings\UserManagementSettings.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="Web.Debug.config">
<DependentUpon>web.config</DependentUpon>
</None>
@ -744,6 +775,7 @@
<EmbeddedResource Include="Resources\UI.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>UI1.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Resources\UI.sv.resx" />
</ItemGroup>

View file

@ -59,11 +59,15 @@ namespace PlexRequests.UI
var baseUrl = result.MapResult(
o => o.BaseUrl,
e => string.Empty);
var port = result.MapResult(
x => x.Port,
e => -1);
var listenerPrefix = result.MapResult(
x => x.ListenerPrefix,
e => string.Empty);
var updated = result.MapResult(x => x.Updated, e => UpdateValue.None);
CheckUpdate(updated);
@ -81,7 +85,12 @@ namespace PlexRequests.UI
if (port == -1 || port == 3579)
port = GetStartupPort();
var options = new StartOptions(Debugger.IsAttached ? $"http://localhost:{port}" : $"http://+:{port}")
if (string.IsNullOrEmpty(listenerPrefix))
{
listenerPrefix = "+";
}
var options = new StartOptions(Debugger.IsAttached ? $"http://localhost:{port}" : $"http://{listenerPrefix}:{port}")
{
ServerFactory = "Microsoft.Owin.Host.HttpListener"
};

View file

@ -459,4 +459,34 @@
<data name="Requests_ReleaseDate_Unavailable" xml:space="preserve">
<value>Der er ingen oplysninger om udgivelsesdatoen</value>
</data>
<data name="Search_ViewInPlex" xml:space="preserve">
<value>Udsigt I Plex</value>
</data>
<data name="Custom_Donation_Default" xml:space="preserve">
<value>Doner til Bibliotek vedligeholder</value>
</data>
<data name="Search_Available_on_plex" xml:space="preserve">
<value>Tilgængelig på</value>
</data>
<data name="Search_Movie_Status" xml:space="preserve">
<value>Movie status</value>
</data>
<data name="Search_Not_Requested_Yet" xml:space="preserve">
<value>Ikke anmodet endnu</value>
</data>
<data name="Search_Pending_approval" xml:space="preserve">
<value>Afventer godkendelse</value>
</data>
<data name="Search_Processing_Request" xml:space="preserve">
<value>Behandler forespørgsel</value>
</data>
<data name="Search_Request_denied" xml:space="preserve">
<value>Anmodning afvist!</value>
</data>
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>Tv-show status!</value>
</data>
<data name="Layout_CacherRunning" xml:space="preserve">
<value>En baggrund proces kører i øjeblikket, så der kan være nogle uventede problemer. Dette bør ikke tage for lang tid.</value>
</data>
</root>

View file

@ -459,4 +459,34 @@
<data name="Requests_ReleaseDate_Unavailable" xml:space="preserve">
<value>Es gibt noch keine Informationen für diesen Release-Termin</value>
</data>
</root>
<data name="Search_ViewInPlex" xml:space="preserve">
<value>Ansicht In Plex</value>
</data>
<data name="Custom_Donation_Default" xml:space="preserve">
<value>Spenden zur Bibliothek Maintainer</value>
</data>
<data name="Search_Available_on_plex" xml:space="preserve">
<value>Verfügbar auf Plex</value>
</data>
<data name="Search_Movie_Status" xml:space="preserve">
<value>Film-Status!</value>
</data>
<data name="Search_Not_Requested_Yet" xml:space="preserve">
<value>Noch nicht heraus!</value>
</data>
<data name="Search_Pending_approval" xml:space="preserve">
<value>Genehmigung ausstehend</value>
</data>
<data name="Search_Processing_Request" xml:space="preserve">
<value>Die Verarbeitung Anfrage</value>
</data>
<data name="Search_Request_denied" xml:space="preserve">
<value>Anfrage verweigert.</value>
</data>
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>TV-Show-Status</value>
</data>
<data name="Layout_CacherRunning" xml:space="preserve">
<value>Ein Hintergrundprozess gerade läuft, so könnte es einige unerwartete Verhalten sein. Dies sollte nicht zu lange dauern.</value>
</data>
</root>

View file

@ -459,4 +459,31 @@
<data name="Requests_ReleaseDate_Unavailable" xml:space="preserve">
<value>No hay información disponible para la fecha de lanzamiento</value>
</data>
<data name="Custom_Donation_Default" xml:space="preserve">
<value>Donar a la biblioteca Mantenedor</value>
</data>
<data name="Search_Available_on_plex" xml:space="preserve">
<value>Disponible en Plex</value>
</data>
<data name="Search_Movie_Status" xml:space="preserve">
<value>estado de película</value>
</data>
<data name="Search_Not_Requested_Yet" xml:space="preserve">
<value>No solicitado</value>
</data>
<data name="Search_Pending_approval" xml:space="preserve">
<value>PENDIENTES DE APROBACIÓN</value>
</data>
<data name="Search_Processing_Request" xml:space="preserve">
<value>solicitud de procesamiento</value>
</data>
<data name="Search_Request_denied" xml:space="preserve">
<value>Solicitud denegada</value>
</data>
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>estado de programa de televisión</value>
</data>
<data name="Layout_CacherRunning" xml:space="preserve">
<value>Un proceso en segundo plano se está ejecutando actualmente, por lo que podría ser un comportamiento inesperado. Esto no debería tomar mucho tiempo.</value>
</data>
</root>

View file

@ -459,4 +459,34 @@
<data name="Requests_ReleaseDate_Unavailable" xml:space="preserve">
<value>Il n'y a pas d'information disponible pour la date de sortie</value>
</data>
<data name="Search_ViewInPlex" xml:space="preserve">
<value>Voir Dans Plex</value>
</data>
<data name="Custom_Donation_Default" xml:space="preserve">
<value>Faire un don à la bibliothèque mainteneur!</value>
</data>
<data name="Search_Available_on_plex" xml:space="preserve">
<value>Disponible sur Plex</value>
</data>
<data name="Search_Movie_Status" xml:space="preserve">
<value>le statut de film</value>
</data>
<data name="Search_Not_Requested_Yet" xml:space="preserve">
<value>Pas encore demandé</value>
</data>
<data name="Search_Pending_approval" xml:space="preserve">
<value>En attente de validation</value>
</data>
<data name="Search_Processing_Request" xml:space="preserve">
<value>Traitement de la demande</value>
</data>
<data name="Search_Request_denied" xml:space="preserve">
<value>Requête refusée</value>
</data>
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>TV show status</value>
</data>
<data name="Layout_CacherRunning" xml:space="preserve">
<value>Un processus d'arrière-plan est en cours d'exécution, de sorte qu'il pourrait y avoir des comportements inattendus. Cela ne devrait pas prendre trop de temps.</value>
</data>
</root>

View file

@ -459,4 +459,34 @@
<data name="Requests_ReleaseDate_Unavailable" xml:space="preserve">
<value>Non ci sono informazioni disponibili per la data di uscita</value>
</data>
<data name="Search_ViewInPlex" xml:space="preserve">
<value>Guarda In Plex</value>
</data>
<data name="Custom_Donation_Default" xml:space="preserve">
<value>Donare alla libreria Maintainer</value>
</data>
<data name="Search_Available_on_plex" xml:space="preserve">
<value>Disponibile su Plex</value>
</data>
<data name="Search_Movie_Status" xml:space="preserve">
<value>lo stato di film</value>
</data>
<data name="Search_Not_Requested_Yet" xml:space="preserve">
<value>Non ancora richiesto</value>
</data>
<data name="Search_Pending_approval" xml:space="preserve">
<value>Approvazione in sospeso</value>
</data>
<data name="Search_Processing_Request" xml:space="preserve">
<value>Elaborazione richiesta</value>
</data>
<data name="Search_Request_denied" xml:space="preserve">
<value>Richiesta negata</value>
</data>
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>Show televisivo di stato</value>
</data>
<data name="Layout_CacherRunning" xml:space="preserve">
<value>Un processo in background è in esecuzione, quindi ci potrebbero essere alcuni comportamenti imprevisti. Questo non dovrebbe richiedere troppo tempo.</value>
</data>
</root>

View file

@ -459,4 +459,34 @@
<data name="Requests_ReleaseDate_Unavailable" xml:space="preserve">
<value>Er is geen informatie beschikbaar voor de release datum</value>
</data>
<data name="Search_ViewInPlex" xml:space="preserve">
<value>Bekijk In Plex</value>
</data>
<data name="Custom_Donation_Default" xml:space="preserve">
<value>Doneer aan Library Maintainer</value>
</data>
<data name="Search_Available_on_plex" xml:space="preserve">
<value>Beschikbaar vanaf</value>
</data>
<data name="Search_Movie_Status" xml:space="preserve">
<value>Movie-status!</value>
</data>
<data name="Search_Not_Requested_Yet" xml:space="preserve">
<value>Nog niet gevraagd</value>
</data>
<data name="Search_Pending_approval" xml:space="preserve">
<value>Wacht op goedkeuring</value>
</data>
<data name="Search_Processing_Request" xml:space="preserve">
<value>Verwerking verzoek...</value>
</data>
<data name="Search_Request_denied" xml:space="preserve">
<value>Aanvraag afgewezen</value>
</data>
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>TV-show-status</value>
</data>
<data name="Layout_CacherRunning" xml:space="preserve">
<value>Een achtergrond taak is momenteel actief, dus er misschien een onverwachte gedrag. Dit moet niet te lang duren.</value>
</data>
</root>

View file

@ -459,4 +459,31 @@
<data name="Requests_ReleaseDate_Unavailable" xml:space="preserve">
<value>Não há informações disponíveis para a data de lançamento</value>
</data>
<data name="Search_ViewInPlex" xml:space="preserve">
<value>Ver Em Plex</value>
</data>
<data name="Custom_Donation_Default" xml:space="preserve">
<value>Doações para Biblioteca Mantenedor</value>
</data>
<data name="Search_Available_on_plex" xml:space="preserve">
<value>Disponível em Plex</value>
</data>
<data name="Search_Movie_Status" xml:space="preserve">
<value>status de filme</value>
</data>
<data name="Search_Not_Requested_Yet" xml:space="preserve">
<value>Não solicitada ainda</value>
</data>
<data name="Search_Pending_approval" xml:space="preserve">
<value>B P/ Aprovação</value>
</data>
<data name="Search_Processing_Request" xml:space="preserve">
<value>Processando solicitação...</value>
</data>
<data name="Search_Request_denied" xml:space="preserve">
<value>Solicitação negada.</value>
</data>
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>Programa de TV status</value>
</data>
</root>

View file

@ -467,4 +467,10 @@
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>TV show status</value>
</data>
</root>
<data name="Layout_CacherRunning" xml:space="preserve">
<value>Currently we are indexing all of the available tv shows and movies on the Plex server, so there might be some unexpected behavior. This shouldn't take too long.</value>
</data>
<data name="Layout_Usermanagement" xml:space="preserve">
<value>User Management</value>
</data>
</root>

View file

@ -459,4 +459,34 @@
<data name="Requests_ReleaseDate_Unavailable" xml:space="preserve">
<value>Det finns ingen information tillgänglig för release datum</value>
</data>
<data name="Search_ViewInPlex" xml:space="preserve">
<value>Vy I Plex</value>
</data>
<data name="Custom_Donation_Default" xml:space="preserve">
<value>Donera till bibliotek Ansvarig</value>
</data>
<data name="Search_Available_on_plex" xml:space="preserve">
<value>Tillgänglig den</value>
</data>
<data name="Search_Movie_Status" xml:space="preserve">
<value>Film status</value>
</data>
<data name="Search_Not_Requested_Yet" xml:space="preserve">
<value>Inte Begärd ännu</value>
</data>
<data name="Search_Pending_approval" xml:space="preserve">
<value>Väntar på godkännande</value>
</data>
<data name="Search_Processing_Request" xml:space="preserve">
<value>Bearbetning förfrågan</value>
</data>
<data name="Search_Request_denied" xml:space="preserve">
<value>Förfrågan nekad</value>
</data>
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>Visa status</value>
</data>
<data name="Layout_CacherRunning" xml:space="preserve">
<value>En bakgrundsprocess är igång, så det kan finnas några oväntade beteende. Detta bör inte ta alltför lång tid.</value>
</data>
</root>

View file

@ -222,6 +222,15 @@ namespace PlexRequests.UI.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to A background process is currently running, so there might be some unexpected behavior. This shouldn&apos;t take too long..
/// </summary>
public static string Layout_CacherRunning {
get {
return ResourceManager.GetString("Layout_CacherRunning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Change Password.
/// </summary>
@ -393,6 +402,15 @@ namespace PlexRequests.UI.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to User Management.
/// </summary>
public static string Layout_Usermanagement {
get {
return ResourceManager.GetString("Layout_Usermanagement", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Welcome.
/// </summary>

View file

@ -59,5 +59,15 @@ namespace PlexRequests.UI.Start
[Option('u', "updated", Required = false, HelpText = "This should only be used by the internal application")]
public UpdateValue Updated { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="StartupOptions"/> is updated.
/// </summary>
/// <value>
/// <c>true</c> if updated; otherwise, <c>false</c>.
/// </value>
[Option('l', "listenerprefix", Required = false, HelpText = "To change the prefix for the listener")]
public string ListenerPrefix { get; set; }
}
}

View file

@ -32,8 +32,11 @@ using Ninject.Planning.Bindings.Resolvers;
using NLog;
using Owin;
using PlexRequests.Core;
using PlexRequests.Core.Migration;
using PlexRequests.Services.Jobs;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Jobs;
using PlexRequests.UI.NinjectModules;
@ -57,7 +60,7 @@ namespace PlexRequests.UI
Debug.WriteLine("Modules found finished.");
var kernel = new StandardKernel(modules);
var kernel = new StandardKernel(new NinjectSettings { InjectNonPublic = true }, modules);
Debug.WriteLine("Created Kernel and Injected Modules");
Debug.WriteLine("Added Contravariant Binder");
@ -75,10 +78,18 @@ namespace PlexRequests.UI
Debug.WriteLine("Settings up Scheduler");
var scheduler = new Scheduler();
scheduler.StartScheduler();
//var c = kernel.Get<IRecentlyAdded>();
//c.Test();
// Reset any jobs running
var jobSettings = kernel.Get<IRepository<ScheduledJobs>>();
var all = jobSettings.GetAll();
foreach (var scheduledJobse in all)
{
scheduledJobse.Running = false;
jobSettings.Update(scheduledJobse);
}
scheduler.StartScheduler();

View file

@ -44,7 +44,7 @@ namespace PlexRequests.UI.Validators
RuleFor(x => x.BaseUrl).NotEqual("login", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'login' as this is reserved by the application.");
RuleFor(x => x.BaseUrl).NotEqual("test", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'test' as this is reserved by the application.");
RuleFor(x => x.BaseUrl).NotEqual("approval", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'approval' as this is reserved by the application.");
RuleFor(x => x.BaseUrl).NotEqual("updatechecker", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'updatechecker' as this is reserved by the application.");
RuleFor(x => x.BaseUrl).NotEqual("layout", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'layout' as this is reserved by the application.");
RuleFor(x => x.BaseUrl).NotEqual("usermanagement", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'usermanagement' as this is reserved by the application.");
RuleFor(x => x.BaseUrl).NotEqual("api", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'api' as this is reserved by the application.");
RuleFor(x => x.BaseUrl).NotEqual("landing", StringComparer.CurrentCultureIgnoreCase).WithMessage("You cannot use 'landing' as this is reserved by the application.");

View file

@ -1,13 +1,17 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@{
var baseUrl = Html.GetBaseUrl();
var formAction = "/admin/authentication";
var usermanagement = "/usermanagement";
if (!string.IsNullOrEmpty(baseUrl.ToHtmlString()))
{
formAction = "/" + baseUrl.ToHtmlString() + formAction;
usermanagement = "/" + baseUrl.ToHtmlString() + usermanagement;
}
}
<div class="col-sm-8 col-sm-push-1">
<form class="form-horizontal" method="POST" action="@formAction" id="mainForm">
@ -50,18 +54,10 @@
<br />
<a href="@usermanagement" class="btn btn-info-outline">User Management</a>
<br />
<p class="form-group">Current users that are allowed to authenticate: </p>
<div class="form-group">
<select id="users" multiple="" class="form-control-custom" style="height: 180px;"></select>
</div>
<div class="form-group">
<div>
<button id="refreshUsers" class="btn btn-primary-outline">Refresh Users</button>
</div>
</div>
<br />
<p class="form-group">A comma separated list of users that you do not want to login.</p>
<div class="form-group">

View file

@ -1,6 +1,6 @@
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.CouchPotatoSettings>
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@{
int port;
if (Model.Port == 0)

View file

@ -2,7 +2,7 @@
@using PlexRequests.Core.Models
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.EmailNotificationSettings>
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@{
int port;
if (Model.EmailPort == 0)

View file

@ -1,5 +1,5 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@{
int port;
if (Model.Port == 0)

View file

@ -1,5 +1,5 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.LandingPageSettings>
@Html.LoadDateTimePickerAsset()
<div class="col-sm-8 col-sm-push-1">

View file

@ -1,5 +1,5 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@Html.LoadTableAssets()
@{
@ -21,7 +21,7 @@
<label for="logLevel" class="control-label">Log Level</label>
<div id="logLevel">
<select class="form-control" id="selected">
<option id="Trace" value="0">Trace</option>
<option id="Trace" value="0">Trace (ONLY USE FOR DEVELOPMENT)</option>
<option id="Debug" value="1">Debug</option>
<option id="Info" value="2">Info</option>
<option id="Warn" value="3">Warn</option>

View file

@ -2,7 +2,7 @@
@using PlexRequests.Core.Models
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.NewletterSettings>
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
<div class="col-sm-8 col-sm-push-1">
<form class="form-horizontal" method="POST" id="mainForm">

View file

@ -2,7 +2,7 @@
@using PlexRequests.Core.Models
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.NotificationSettingsV2>
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
<div class="col-sm-8 col-sm-push-1">
<form class="form-horizontal" method="POST" id="mainForm">

View file

@ -1,6 +1,6 @@
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.PlexSettings>
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@{
int port;
if (Model.Port == 0)
@ -87,21 +87,6 @@
<input type="text" class="form-control form-control-custom " id="SubDir" name="SubDir" value="@Model.SubDir">
</div>
</div>
@*<div class="form-group">
<label for="PlexDatabaseLocationOverride" class="control-label">Plex Database Override</label>
<div>
<input type="text" class="form-control form-control-custom " id="PlexDatabaseLocationOverride" name="PlexDatabaseLocationOverride" placeholder="%LOCALAPPDATA%\Plex Media Server\" value="@Model.PlexDatabaseLocationOverride">
</div>
<small>
This is your Plex data directory location, if we cannot manually find it then you need to specify the location! See <a href="https://support.plex.tv/hc/en-us/articles/202915258-Where-is-the-Plex-Media-Server-data-directory-located-">Here</a>.
</small>
</div>
<div class="form-group">
<div class="">
<button id="dbTest" class="btn btn-primary-outline">Test Database Directory <i class="fa fa-database"></i> <div id="dbSpinner"></div></button>
</div>
</div>*@
<div class="form-group">
<label for="authToken" class="control-label">Plex Authorization Token</label>
@ -110,6 +95,13 @@
</div>
</div>
<div class="form-group">
<label for="MachineIdentifier" class="control-label">Machine Identifier</label>
<div class="">
<input type="text" class="form-control-custom form-control" id="MachineIdentifier" name="MachineIdentifier" value="@Model.MachineIdentifier">
</div>
</div>
<div class="form-group">

View file

@ -1,5 +1,5 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
<div class="col-sm-8 col-sm-push-1">
<form class="form-horizontal" method="POST" id="mainForm">

View file

@ -1,5 +1,5 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
<div class="col-sm-8 col-sm-push-1">
<form class="form-horizontal" method="POST" id="mainForm">

View file

@ -1,6 +1,6 @@
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.UI.Models.ScheduledJobsViewModel>
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
<div class="col-sm-8 col-sm-push-1">
@ -32,6 +32,16 @@
<input type="text" class="form-control form-control-custom " id="PlexAvailabilityChecker" name="PlexAvailabilityChecker" value="@Model.PlexAvailabilityChecker">
</div>
<div class="form-group">
<label for="PlexContentCacher" class="control-label">Plex Content Cacher (min)</label>
<input type="text" class="form-control form-control-custom " id="PlexContentCacher" name="PlexContentCacher" value="@Model.PlexContentCacher">
</div>
<div class="form-group">
<label for="PlexUserChecker" class="control-label">Plex User Checker (hours)</label>
<input type="text" class="form-control form-control-custom " id="PlexUserChecker" name="PlexUserChecker" value="@Model.PlexUserChecker">
</div>
<div class="form-group">
<label for="CouchPotatoCacher" class="control-label">Couch Potato Cacher (min)</label>
<input type="text" class="form-control form-control-custom " id="CouchPotatoCacher" name="CouchPotatoCacher" value="@Model.CouchPotatoCacher">

View file

@ -1,6 +1,6 @@
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.PlexRequestSettings>
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@{
int port;
if (Model.Port == 0)
@ -57,11 +57,17 @@
<div class="form-group">
<label for="ApiKey" class="control-label">Api Key</label>
<div class="input-group">
<input type="text" disabled="disabled" class="form-control form-control-custom" id="apiKey" name="ApiKey" value="@Model.ApiKey">
<input type="text" hidden="hidden" name="ApiKey" value="@Model.ApiKey"/>
<input type="text" readonly="readonly" class="form-control form-control-custom" id="ApiKey" name="ApiKey" value="@Model.ApiKey">
<div class="input-group-addon">
<div id="refreshKey" class="fa fa-refresh" title="Reset API Key"></div>
</div>
<div class="input-group-addon">
<div class="fa fa-clipboard" data-clipboard-action="copy" data-clipboard-target="#ApiKey"></div>
</div>
</div>
</div>
@ -74,20 +80,9 @@
</select>
</div>
</div>
<div class="form-group">
<div class="checkbox">
@Html.Checkbox(Model.SearchForMovies,"SearchForMovies","Search for Movies")
@if (Model.SearchForMovies)
{
<input type="checkbox" id="SearchForMovies" name="SearchForMovies" checked="checked"><label for="SearchForMovies">Search for Movies</label>
}
else
{
<input type="checkbox" id="SearchForMovies" name="SearchForMovies"><label for="SearchForMovies">Search for Movies</label>
}
</div>
</div>
<div class="form-group">
<div class="checkbox">
@ -117,82 +112,7 @@
</div>
</div>
<div class="form-group">
<div class="checkbox">
@if (Model.RequireMovieApproval)
{
<input type="checkbox" id="RequireMovieApproval" name="RequireMovieApproval" checked="checked"><label for="RequireMovieApproval">Require approval of Movie requests</label>
}
else
{
<input type="checkbox" id="RequireMovieApproval" name="RequireMovieApproval"><label for="RequireMovieApproval">Require approval of Movie requests</label>
}
</div>
</div>
<div class="form-group">
<div class="checkbox">
@if (Model.RequireTvShowApproval)
{
<input type="checkbox" id="RequireTvShowApproval" name="RequireTvShowApproval" checked="checked"><label for="RequireTvShowApproval">Require approval of TV show requests</label>
}
else
{
<input type="checkbox" id="RequireTvShowApproval" name="RequireTvShowApproval"><label for="RequireTvShowApproval">Require approval of TV show requests</label>
}
</div>
</div>
<div class="form-group">
<div class="checkbox">
@if (Model.RequireMusicApproval)
{
<input type="checkbox" id="RequireMusicApproval" name="RequireMusicApproval" checked="checked"><label for="RequireMusicApproval">Require approval of Music requests</label>
}
else
{
<input type="checkbox" id="RequireMusicApproval" name="RequireMusicApproval"><label for="RequireMusicApproval">Require approval of Music requests</label>
}
</div>
</div>
<div class="form-group">
<div class="checkbox">
@if (Model.UsersCanViewOnlyOwnRequests)
{
<input type="checkbox" id="UsersCanViewOnlyOwnRequests" name="UsersCanViewOnlyOwnRequests" checked="checked">
<label for="UsersCanViewOnlyOwnRequests">Users can view their own requests only</label>
}
else
{
<input type="checkbox" id="UsersCanViewOnlyOwnRequests" name="UsersCanViewOnlyOwnRequests"><label for="UsersCanViewOnlyOwnRequests">Users can view their own requests only</label>
}
</div>
</div>
<div class="form-group">
<div class="checkbox">
@if (Model.UsersCanViewOnlyOwnIssues)
{
<input type="checkbox" id="UsersCanViewOnlyOwnIssues" name="UsersCanViewOnlyOwnIssues" checked="checked">
<label for="UsersCanViewOnlyOwnIssues">Users can view their own issues only</label>
}
else
{
<input type="checkbox" id="UsersCanViewOnlyOwnIssues" name="UsersCanViewOnlyOwnIssues"><label for="UsersCanViewOnlyOwnIssues">Users can view their own issues only</label>
}
</div>
</div>
<div class="form-group">
<div class="checkbox">
@ -278,38 +198,25 @@
<input type="text" class="form-control-custom form-control " id="CustomDonationMessage" name="CustomDonationMessage" placeholder="Donation button message" value="@Model.CustomDonationMessage">
</div>
</div>
<p class="form-group">A comma separated list of users whose requests do not require approval (These users also do not have a request limit).</p>
<p class="form-group">If the request limits are set to 0 then no request limit is applied.</p>
<div class="form-group">
<label for="NoApprovalUsers" class="control-label">Approval White listed Users</label>
<label for="MovieWeeklyRequestLimit" class="control-label">Movie Weekly Request Limit</label>
<div>
<input type="text" class="form-control-custom form-control " id="NoApprovalUsers" name="NoApprovalUsers" placeholder="e.g. John, Bobby" value="@Model.NoApprovalUsers">
<label>
<input type="number" id="MovieWeeklyRequestLimit" name="MovieWeeklyRequestLimit" class="form-control form-control-custom " value="@Model.MovieWeeklyRequestLimit">
</label>
</div>
</div>
<p class="form-group">If the request limits are set to 0 then no request limit is applied.</p>
<div class="form-group">
<label for="MovieWeeklyRequestLimit" class="control-label">Movie Weekly Request Limit</label>
<div>
<label>
<input type="number" id="MovieWeeklyRequestLimit" name="MovieWeeklyRequestLimit" class="form-control form-control-custom " value="@Model.MovieWeeklyRequestLimit">
</label>
<div class="form-group">
<label for="TvWeeklyRequestLimit" class="control-label">TV Show Weekly Request Limit</label>
<div>
<label>
<input type="number" id="TvWeeklyRequestLimit" name="TvWeeklyRequestLimit" class="form-control form-control-custom " value="@Model.TvWeeklyRequestLimit">
</label>
</div>
</div>
</div>
<div class="form-group">
<label for="TvWeeklyRequestLimit" class="control-label">TV Show Weekly Request Limit</label>
<div>
<label>
<input type="number" id="TvWeeklyRequestLimit" name="TvWeeklyRequestLimit" class="form-control form-control-custom " value="@Model.TvWeeklyRequestLimit">
</label>
</div>
</div>
<div class="form-group">
<label for="AlbumWeeklyRequestLimit" class="control-label">Album Weekly Request Limit</label>
@ -320,7 +227,7 @@
</div>
</div>
<div>
<div>
</div>
<div class="form-group">
<div>
@ -333,6 +240,11 @@
<script>
$(function () {
new Clipboard('.fa-clipboard');
$('#save').click(function (e) {
e.preventDefault();
@ -373,7 +285,7 @@
success: function(response) {
if (response) {
generateNotify("Success!", "success");
$('#apiKey').val(response);
$('#ApiKey').val(response);
}
},
error: function(e) {

View file

@ -1,5 +1,5 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@{
int port;
if (Model.Port == 0)

View file

@ -1,5 +1,5 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
<div class="col-sm-8 col-sm-push-1">
<form class="form-horizontal" method="POST" id="mainForm">

View file

@ -1,5 +1,5 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
@Html.Partial("Shared/Partial/_Sidebar")
@{
int port;
if (Model.Port == 0)

View file

@ -1,71 +0,0 @@
@using PlexRequests.UI.Helpers
@Html.Partial("_Sidebar")
<div class="col-sm-8 col-sm-push-1">
<fieldset>
<legend>Status</legend>
<div class="form-group">
<label class="control-label">Version: </label>
<label class="control-label">@Model.Version</label>
</div>
<div class="form-group">
<label class="control-label">Update Available: </label>
@if (Model.UpdateAvailable)
{
<label class="control-label"><a href="@Model.UpdateUri" target="_blank"><i class="fa fa-check"></i></a></label>
<br />
@*<button id="autoUpdate" class="btn btn-success-outline">Automatic Update <i class="fa fa-download"></i></button>*@ //TODO
}
else
{
<label class="control-label"><i class="fa fa-times"></i></label>
}
</div>
@if (Model.UpdateAvailable)
{
<h2>
<a href="@Model.DownloadUri">@Model.ReleaseTitle</a>
</h2>
<hr />
<label>Release Notes:</label>
@Html.Raw(Model.ReleaseNotes)
}
</fieldset>
</div>
<script>
var base = '@Html.GetBaseUrl()';
$('#autoUpdate')
.click(function (e) {
e.preventDefault();
$('body').append("<i class=\"fa fa-spinner fa-spin fa-5x fa-fw\" style=\"position: absolute; top: 20%; left: 50%;\"></i>");
$('#autoUpdate').prop("disabled", "disabled");
var count = 0;
setInterval(function () {
count++;
var dots = new Array(count % 10).join('.');
document.getElementById('autoUpdate').innerHTML = "Updating" + dots;
}, 1000);
$.ajax({
type: "Post",
url: "autoupdate",
data: { url: "@Model.DownloadUri" },
dataType: "json",
error: function () {
setTimeout(
function () {
location.reload();
}, 30000);
}
});
});
</script>

View file

@ -0,0 +1,109 @@
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<IEnumerable<PlexRequests.UI.Models.FaultedRequestsViewModel>>
@Html.Partial("Shared/Partial/_Sidebar")
<div class="col-sm-8 col-sm-push-1">
<fieldset>
<legend>Release Fault Queue</legend>
<table class="table table-striped table-hover table-responsive table-condensed">
<thead>
<tr>
<th>
Request Title
</th>
<th>
Type
</th>
<th>
Fault Type
</th>
<th>
LastRetry
</th>
<th>
Error Description
</th>
</tr>
</thead>
<tbody>
@foreach (var m in Model)
{
<tr>
<td>
@m.Title
</td>
<td>
@m.Type
</td>
<td>
@m.FaultType
</td>
<td>
@m.LastRetry
</td>
<td>
@m.Message
</td>
</tr>
}
</tbody>
</table>
</fieldset>
</div>
@*<script>
var base = '@Html.GetBaseUrl()';
$('#autoUpdate')
.click(function (e) {
e.preventDefault();
$('body').append("<i class=\"fa fa-spinner fa-spin fa-5x fa-fw\" style=\"position: absolute; top: 20%; left: 50%;\"></i>");
$('#autoUpdate').prop("disabled", "disabled");
document.getElementById("lightbox").style.display = "";
var count = 0;
setInterval(function () {
count++;
var dots = new Array(count % 10).join('.');
document.getElementById('autoUpdate').innerHTML = "Updating" + dots;
}, 1000);
$.ajax({
type: "Post",
url: "autoupdate",
data: { url: "@Model.Status.DownloadUri" },
dataType: "json",
error: function () {
setTimeout(
function () {
location.reload();
}, 30000);
}
});
});
$('#saveSettings').click(function (e) {
e.preventDefault();
var $form = $("#mainForm");
var branches = $("#branches option:selected").val();
var data = $form.serialize();
data = data + "&branch=" + branches;
$.ajax({
type: $form.prop("method"),
url: $form.prop("action"),
data: data,
dataType: "json",
success: function (response) {
if (response.result === true) {
generateNotify(response.message, "success");
} else {
generateNotify(response.message, "warning");
}
}
});
});
</script>*@

View file

@ -1,6 +1,7 @@
@using System.Linq
@using PlexRequests.Core.Models
@using PlexRequests.Helpers
@using PlexRequests.Helpers.Permissions
@using PlexRequests.UI.Helpers
@{
var baseUrl = Html.GetBaseUrl();
@ -10,16 +11,8 @@
formAction = "/" + baseUrl.ToHtmlString();
}
var isAdmin = false;
var isAdmin = Html.HasAnyPermission(true, Permissions.Administrator, Permissions.ManageRequests);
if (Context.CurrentUser != null)
{
var claims = Context.CurrentUser.Claims.ToList();
if (claims.Contains("Admin") || claims.Contains("PowerUser"))
{
isAdmin = true;
}
}
}
<h1>Details</h1>

View file

@ -1,19 +1,22 @@
@using Nancy.Security
@using Nancy.Security
@using PlexRequests.Helpers.Permissions
@using PlexRequests.UI.Helpers
@using PlexRequests.UI.Resources
@{
var baseUrl = Html.GetBaseUrl();
var formAction = string.Empty;
var isAdmin = Html.HasAnyPermission(true, Permissions.Administrator, Permissions.ManageRequests);
if (!string.IsNullOrEmpty(baseUrl.ToHtmlString()))
{
formAction = "/" + baseUrl.ToHtmlString();
}
}
<div>
<div hidden="hidden" id="isAdmin" value="@isAdmin"></div>
<h1>@UI.Requests_Title</h1>
<h4>@UI.Requests_Paragraph</h4>
<br />
<br/>
<!-- Nav tabs -->
<ul id="nav-tabs" class="nav nav-tabs" role="tablist">
@ -30,7 +33,7 @@
<li role="presentation"><a href="#MusicTab" aria-controls="profile" role="tab" data-toggle="tab"><i class="fa fa-music"></i> @UI.Requests_AlbumsTabTitle</a></li>
}
</ul>
<br />
<br/>
<!-- Tab panes -->
<div class="tab-content contentList">
@ -38,38 +41,59 @@
<div class="col-sm-12">
<div class="pull-right">
<div class="btn-group btn-group-separated">
@if (Context.CurrentUser.IsAuthenticated()) //TODO replace with IsAdmin
@if (isAdmin)
{
@if (Model.SearchForMovies)
{
<button id="deleteMovies" class="btn btn-warning-outline delete-category" type="submit"><i class="fa fa-trash"></i> @UI.Requests_DeleteMovies</button>
<button id="approveMovies" class="btn btn-success-outline approve-category" type="submit"><i class="fa fa-plus"></i> @UI.Requests_ApproveMovies</button>
}
{
<button id="deleteMovies" class="btn btn-warning-outline delete-category" type="submit"><i class="fa fa-trash"></i> @UI.Requests_DeleteMovies</button>
<button id="approveMovies" class="btn btn-success-outline approve-category" type="submit"><i class="fa fa-plus"></i> @UI.Requests_ApproveMovies</button>
}
@if (Model.SearchForTvShows)
{
<button id="deleteTVShows" class="btn btn-warning-outline delete-category" type="submit" style="display: none;"><i class="fa fa-trash"></i> @UI.Requests_DeleteTVShows</button>
<button id="approveTVShows" class="btn btn-success-outline approve-category" type="submit" style="display: none;"><i class="fa fa-plus"></i> @UI.Requests_ApproveTvShows</button>
}
{
<button id="deleteTVShows" class="btn btn-warning-outline delete-category" type="submit" style="display: none;"><i class="fa fa-trash"></i> @UI.Requests_DeleteTVShows</button>
<button id="approveTVShows" class="btn btn-success-outline approve-category" type="submit" style="display: none;"><i class="fa fa-plus"></i> @UI.Requests_ApproveTvShows</button>
}
@if (Model.SearchForMusic)
{
<button id="deleteMusic" class="btn btn-warning-outline delete-category" type="submit" style="display: none;"><i class="fa fa-trash"></i> @UI.Requests_DeleteMusic</button>
<button id="approveMusic" class="btn btn-success-outline approve-category" type="submit" style="display: none;"><i class="fa fa-plus"></i> @UI.Requests_ApproveMusic</button>
}
{
<button id="deleteMusic" class="btn btn-warning-outline delete-category" type="submit" style="display: none;"><i class="fa fa-trash"></i> @UI.Requests_DeleteMusic</button>
<button id="approveMusic" class="btn btn-success-outline approve-category" type="submit" style="display: none;"><i class="fa fa-plus"></i> @UI.Requests_ApproveMusic</button>
}
}
</div>
<div class="btn-group">
<a href="#" class="btn btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
@UI.Requests_Filter
@if (isAdmin)
{
<span id="filterText">@UI.Requests_Filter_NotApproved</span>
}
else
{
<span id="filterText">@UI.Requests_Filter_All</span>
}
<i class="fa fa-filter"></i>
</a>
<ul class="dropdown-menu">
<li><a href="#" class="filter" data-filter="all"><i class="fa fa-check-square"></i> @UI.Requests_Filter_All</a></li>
<li><a href="#" class="filter" data-filter=".approved-true"><i class="fa fa-square-o"></i> @UI.Requests_Filter_Approved</a></li>
<li><a href="#" class="filter" data-filter=".approved-false"><i class="fa fa-square-o"></i> @UI.Requests_Filter_NotApproved</a></li>
<li><a href="#" class="filter" data-filter=".available-true"><i class="fa fa-square-o"></i> @UI.Requests_Filter_Available</a></li>
@if (!isAdmin)
{
<li><a href="#" class="filter" data-filter="all"><i class="fa fa-check-square"></i> @UI.Requests_Filter_All</a></li>
}
else
{
<li><a href="#" class="filter" data-filter="all"><i class="fa fa-square-o"></i> @UI.Requests_Filter_All</a></li>
}
<li><a href="#" class="filter" data-filter=".approved-true"><i class="fa fa-square-o"></i> @UI.Requests_Filter_Approved</a></li>
@if (isAdmin)
{
<li><a href="#" class="filter" data-filter=".approved-false"><i class="fa fa-check-square"></i> @UI.Requests_Filter_NotApproved</a></li>
}
else
{
<li><a href="#" class="filter" data-filter=".approved-false"><i class="fa fa-square-o"></i> @UI.Requests_Filter_NotApproved</a></li>
}
<li><a href="#" class="filter" data-filter=".available-true"><i class="fa fa-square-o"></i> @UI.Requests_Filter_Available</a></li>
<li><a href="#" class="filter" data-filter=".available-false"><i class="fa fa-square-o"></i> @UI.Requests_Filter_NotAvailable</a></li>
<li><a href="#" class="filter" data-filter=".released-true"><i class="fa fa-square-o"></i> @UI.Requests_Filter_Released</a></li>
<li><a href="#" class="filter" data-filter=".released-false"><i class="fa fa-square-o"></i> @UI.Requests_Filter_NotReleased</a></li>
<li><a href="#" class="filter" data-filter=".released-true"><i class="fa fa-square-o"></i> @UI.Requests_Filter_Released</a></li>
<li><a href="#" class="filter" data-filter=".released-false"><i class="fa fa-square-o"></i> @UI.Requests_Filter_NotReleased</a></li>
</ul>
</div>
<div class="btn-group">
@ -78,23 +102,23 @@
<i class="fa fa-sort"></i>
</a>
<ul class="dropdown-menu">
<li><a href="#" class="sort" data-sort="requestorder:desc"><i class="fa fa-check-square"></i> @UI.Requests_Order_LatestRequests</a></li>
<li><a href="#" class="sort" data-sort="requestorder:asc"><i class="fa fa-square-o"></i> @UI.Requests_Order_OldestRequests</a></li>
<li><a href="#" class="sort" data-sort="releaseorder:desc"><i class="fa fa-square-o"></i> @UI.Requests_Order_LatestReleases</a></li>
<li><a href="#" class="sort" data-sort="releaseorder:asc"><i class="fa fa-square-o"></i> @UI.Requests_Order_OldestReleases</a></li>
<li><a href="#" class="sort" data-sort="requestorder:desc"><i class="fa fa-check-square"></i> @UI.Requests_Order_LatestRequests</a></li>
<li><a href="#" class="sort" data-sort="requestorder:asc"><i class="fa fa-square-o"></i> @UI.Requests_Order_OldestRequests</a></li>
<li><a href="#" class="sort" data-sort="releaseorder:desc"><i class="fa fa-square-o"></i> @UI.Requests_Order_LatestReleases</a></li>
<li><a href="#" class="sort" data-sort="releaseorder:asc"><i class="fa fa-square-o"></i> @UI.Requests_Order_OldestReleases</a></li>
</ul>
</div>
</div>
</div>
</div>
@if (Model.SearchForMovies)
{
{
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab">
<br />
<br />
<br/>
<br/>
<!-- Movie content -->
<div id="movieList">
</div>
@ -102,12 +126,12 @@
}
@if (Model.SearchForTvShows)
{
{
<!-- TV tab -->
<div role="tabpanel" class="tab-pane" id="TvShowTab">
<br />
<br />
<br/>
<br/>
<!-- TV content -->
<div id="tvList">
</div>
@ -115,12 +139,12 @@
}
@if (Model.SearchForMusic)
{
{
<!-- Music tab -->
<div role="tabpanel" class="tab-pane" id="MusicTab">
<br />
<br />
<br/>
<br/>
<!-- TV content -->
<div id="musicList">
</div>
@ -168,38 +192,20 @@
<a href="http://www.imdb.com/title/{{imdb}}/" target="_blank">
<h4 class="request-title">{{title}} ({{year}})</h4>
</a>
<div>
{{#if_eq type "tv"}}
<span>@UI.Search_TV_Show_Status: </span>
{{else}}
<span>@UI.Search_Movie_Status: </span>
{{/if_eq}}
<span class="label label-success">{{status}}</span>
</div>
<div>
<span>Request status: </span>
{{#if available}}
<span class="label label-success">@UI.Search_Available_on_plex</span>
{{else}}
{{#if approved}}
<span class="label label-info">@UI.Search_Processing_Request</span>
{{else if denied}}
<span class="label label-danger">@UI.Search_Request_denied</span>
{{#if deniedReason}}
<span class="customTooltip" title="{{deniedReason}}"><i class="fa fa-info-circle"></i></span>
{{/if}}
{{else}}
<span class="label label-warning">@UI.Search_Pending_approval</span>
{{/if}}
{{/if}}
</div>
</div>
<br />
{{#if_eq type "tv"}}
<span>@UI.Search_TV_Show_Status: </span>
{{else}}
<span>@UI.Search_Movie_Status: </span>
{{/if_eq}}
<span class="label label-success">{{status}}</span>
{{#if denied}}
<div>
Denied: <i style="color:red;" class="fa fa-check"></i>
{{#if deniedReason}}
<span class="customTooltip" title="{{deniedReason}}"><i class="fa fa-info-circle"></i></span>
{{/if}}
</div>
{{/if}}
@ -208,7 +214,26 @@
{{else}}
<div>@UI.Requests_ReleaseDate: {{releaseDate}}</div>
{{/if_eq}}
<br />
{{#unless denied}}
<div>
@UI.Common_Approved:
{{#if_eq approved false}}
<i id="{{requestId}}notapproved" class="fa fa-times"></i>
{{/if_eq}}
{{#if_eq approved true}}
<i class="fa fa-check"></i>
{{/if_eq}}
</div>
{{/unless}}
<div>
@UI.Requests_Available
{{#if_eq available false}}
<i id="availableIcon{{requestId}}" class="fa fa-times"></i>
{{/if_eq}}
{{#if_eq available true}}
<i id="availableIcon{{requestId}}" class="fa fa-check"></i>
{{/if_eq}}
</div>
{{#if_eq type "tv"}}
{{#if episodes}}

View file

@ -111,33 +111,6 @@
</div>
}
<!-- Notification tab -->
<div role="tabpanel" class="tab-pane" id="NotificationsTab">
<div class="input-group">
<div class="input-group-addon input-group-sm"></div>
</div>
<br />
<!-- Notifications content -->
<form class="form-horizontal" method="POST" id="notificationsForm">
<fieldset>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="notifyUser" name="Notify">
<label for="notifyUser">@UI.Search_SendNotificationText</label>
</div>
</div>
<div class="form-group">
<div>
<button id="saveNotificationSettings" class="btn btn-primary-outline">@UI.Common_Save</button>
</div>
</div>
</fieldset>
</form>
</div>
</div>
</div>
<!-- Movie and TV Results template -->
<script id="search-template" type="text/x-handlebars-template">
<div class="row">

View file

@ -22,6 +22,8 @@
<meta charset="utf-8">
<!-- Styles -->
<meta name="viewport" content="width=device-width, initial-scale=1">
@Html.LoadFavIcon()
@Html.LoadAnalytics()
@Html.LoadAssets()
</head>

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