mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-21 05:43:19 -07:00
Merge branch 'dev' into master
This commit is contained in:
commit
a056ba17e5
206 changed files with 8047 additions and 2824 deletions
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
409
PlexRequests.UI/Authentication/CustomAuthenticationProvider.cs
Normal file
409
PlexRequests.UI/Authentication/CustomAuthenticationProvider.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
108
PlexRequests.UI/Authentication/CustomModuleExtensions.cs
Normal file
108
PlexRequests.UI/Authentication/CustomModuleExtensions.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 +=
|
||||
|
|
27
PlexRequests.UI/Content/Angular/angular-loading-spinner.js
vendored
Normal file
27
PlexRequests.UI/Content/Angular/angular-loading-spinner.js
vendored
Normal 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);
|
2
PlexRequests.UI/Content/Angular/angular-spinner.min.js
vendored
Normal file
2
PlexRequests.UI/Content/Angular/angular-spinner.min.js
vendored
Normal 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
|
|
@ -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; }
|
||||
|
||||
|
|
2
PlexRequests.UI/Content/Themes/plex.min.css
vendored
2
PlexRequests.UI/Content/Themes/plex.min.css
vendored
|
@ -1 +1 @@
|
|||
.form-control-custom{background-color:#333 !important;}.form-control-custom-disabled{background-color:#252424 !important;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#df691a;}.scroll-top-wrapper{background-color:#333;}.scroll-top-wrapper:hover{background-color:#df691a;}body{font-family:Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif;color:#eee;background-color:#1f1f1f;}.table-striped>tbody>tr:nth-of-type(odd){background-color:#333;}.table-hover>tbody>tr:hover{background-color:#282828;}fieldset{padding:15px;}legend{border-bottom:1px solid #333;}.form-control{color:#fefefe;background-color:#333;}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{margin-left:-0;}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:-15px;}.dropdown-menu{background-color:#282828;}.dropdown-menu .divider{background-color:#333;}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#333;}.input-group-addon{background-color:#333;}.nav>li>a:hover,.nav>li>a:focus{background-color:#df691a;}.nav-tabs>li>a:hover{border-color:#df691a #df691a transparent;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background-color:#df691a;border:1px solid #df691a;}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #df691a;}.navbar-default{background-color:#0a0a0a;}.navbar-default .navbar-brand{color:#df691a;}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#f0ad4e;background-color:#282828;}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{background-color:#282828;}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#df691a;color:#fff;}.pagination>li>a,.pagination>li>span{background-color:#282828;}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#333;}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#fefefe;background-color:#333;}.list-group-item{background-color:#282828;}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{background-color:#333;}.input-addon,.input-group-addon{color:#df691a;}.modal-header,.modal-footer{background-color:#282828;}.modal-content{position:relative;background-color:#282828;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;outline:0;}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:300;color:#ebebeb;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#333;border-radius:10px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#333;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #333 !important;}#sidebar-wrapper{background:#252424;}
|
||||
.form-control-custom{background-color:#333 !important;}.form-control-custom-disabled{background-color:#252424 !important;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#df691a;}.scroll-top-wrapper{background-color:#333;}.scroll-top-wrapper:hover{background-color:#df691a;}body{font-family:Open Sans Regular,Helvetica Neue,Helvetica,Arial,sans-serif;color:#eee;background-color:#1f1f1f;}.table-striped>tbody>tr:nth-of-type(odd){background-color:#333;}.table-hover>tbody>tr:hover{background-color:#282828;}fieldset{padding:15px;}legend{border-bottom:1px solid #333;}.form-control{color:#fefefe;background-color:#333;}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{margin-left:-0;}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:-15px;}.dropdown-menu{background-color:#282828;}.dropdown-menu .divider{background-color:#333;}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#333;}.input-group-addon{background-color:#333;}.nav>li>a:hover,.nav>li>a:focus{background-color:#df691a;}.nav-tabs>li>a:hover{border-color:#df691a #df691a transparent;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background-color:#df691a;border:1px solid #df691a;}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #df691a;}.navbar-default{background-color:#0a0a0a;}.navbar-default .navbar-brand{color:#df691a;}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#f0ad4e;background-color:#282828;}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{background-color:#282828;}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#df691a;color:#fff;}.pagination>li>a,.pagination>li>span{background-color:#282828;}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#333;}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#fefefe;background-color:#333;}.list-group-item{background-color:#282828;}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{background-color:#333;}.input-addon,.input-group-addon{color:#df691a;}.modal-header,.modal-footer{background-color:#282828;}.modal-content{position:relative;background-color:#282828;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;outline:0;}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:300;color:#ebebeb;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#333;border-radius:10px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#333;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #333 !important;}#sidebar-wrapper{background:#252424;}#cacherRunning{background-color:#333;text-align:center;font-size:15px;padding:3px 0;}
|
|
@ -228,3 +228,10 @@ button.list-group-item:focus {
|
|||
#sidebar-wrapper {
|
||||
background: $bg-colour-disabled;
|
||||
}
|
||||
|
||||
#cacherRunning {
|
||||
background-color: $bg-colour;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
padding: 3px 0;
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
(function() {
|
||||
module = angular.module('PlexRequests', []);
|
||||
module = angular.module('PlexRequests', ['ngLoadingSpinner']);
|
||||
module.constant("moment", moment);
|
||||
|
||||
//module.config(['usSpinnerConfigProvider', function (usSpinnerConfigProvider) {
|
||||
// usSpinnerConfigProvider.setDefaults({ color: 'white' });
|
||||
//}]);
|
||||
}());
|
|
@ -0,0 +1,39 @@
|
|||
<form name="userform" ng-submit="addUser()" novalidate>
|
||||
<div class="form-group">
|
||||
<input id="username" type="text" placeholder="user" ng-model="user.username" class="form-control form-control-custom" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input id="password" type="password" placeholder="password" ng-model="user.password" class="form-control form-control-custom" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input id="email" type="email" placeholder="email address" ng-model="user.email" class="form-control form-control-custom" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<h3 class="col-md-5">Permissions: </h3>
|
||||
<h3 class="col-md-5">Features: </h3>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="checkbox" ng-repeat="permission in permissions">
|
||||
<input id="permission_{{$id}}" class="checkbox-custom" name="permission[]"
|
||||
ng-checked="permission.selected" ng-model="permission.selected" type="checkbox" value="{{permission.value}}"/>
|
||||
<label for="permission_{{$id}}">{{permission.name}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<div class="checkbox" ng-repeat="f in features">
|
||||
<input id="features_{{$id}}" class="checkbox-custom" name="f[]"
|
||||
ng-checked="f.selected" ng-model="f.selected" type="checkbox" value="{{f.value}}"/>
|
||||
<label for="features_{{$id}}">{{f.name}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" class="btn btn-success-outline" value="Add" />
|
||||
|
||||
<button type="button" ng-click="redirectToSettings()" class="btn btn-primary-outline" style="float: right; margin-right: 10px;">Settings</button>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,57 @@
|
|||
<!--Sidebar-->
|
||||
<div id="sidebar-wrapper" class="shadow">
|
||||
<div style="margin-left:15px">
|
||||
<br />
|
||||
<br />
|
||||
<img ng-show="selectedUser.plexInfo.thumb" class="col-md-pull-1 img-circle" style="position: absolute" ng-src="{{selectedUser.plexInfo.thumb}}" />
|
||||
<div hidden="hidden" ng-bind="selectedUser.id"></div>
|
||||
<div>
|
||||
<strong>Username: </strong><span ng-bind="selectedUser.username"></span>
|
||||
</div>
|
||||
<div ng-show="selectedUser.emailAddress">
|
||||
<strong>Email Address: </strong><span ng-bind="selectedUser.emailAddress"></span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>User Type: </strong><span ng-bind="selectedUser.type === 1 ? 'Local User' : 'Plex User'"></span>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<div ng-show="selectedUser">
|
||||
<!--Edit-->
|
||||
<div class="row" style="margin-left: 0; margin-right: 0;">
|
||||
<div class="col-md-6">
|
||||
<strong>Modify Permissions:</strong>
|
||||
<!--Load all permissions-->
|
||||
|
||||
<div class="checkbox small-checkbox" ng-repeat="p in selectedUser.permissions">
|
||||
<input id="permissionsCheckbox_{{$id}}" class="checkbox-custom" name="permissions[]" ng-checked="p.selected" ng-model="p.selected" type="checkbox" value="{{p.value}}" />
|
||||
<label class="small-label" for="permissionsCheckbox_{{$id}}">{{p.name}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Modify Features:</strong>
|
||||
<!--Load all features-->
|
||||
<div class="checkbox small-checkbox" ng-repeat="p in selectedUser.features">
|
||||
<input id="featuresCheckbox_{{$id}}" class="checkbox-custom" name="features[]" ng-checked="p.selected" ng-model="p.selected" type="checkbox" value="{{p.value}}" />
|
||||
<label class="small-label" for="featuresCheckbox_{{$id}}">{{p.name}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<strong>Email Address</strong>
|
||||
<div class="form-group">
|
||||
<input id="emailAddress" type="email" ng-model="selectedUser.emailAddress" ng-disabled="selectedUser.type === 0" class="form-control form-control-custom" />
|
||||
</div>
|
||||
|
||||
<strong>Alias</strong>
|
||||
<div class="form-group">
|
||||
<input id="alias" type="text" ng-model="selectedUser.alias" class="form-control form-control-custom" />
|
||||
</div>
|
||||
|
||||
|
||||
<button ng-click="updateUser()" class="btn btn-primary-outline">Update</button>
|
||||
<button ng-click="deleteUser()" class="btn btn-danger-outline">Delete</button>
|
||||
<button ng-click="closeSidebarClick()" style="float: right; margin-right: 10px;" class="btn btn-danger-outline">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--SideBar End-->
|
|
@ -0,0 +1,86 @@
|
|||
|
||||
<!--Search-->
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-search"></i>
|
||||
</div>
|
||||
|
||||
<input type="text" class="form-control" placeholder="Search" ng-model="searchTerm">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Table -->
|
||||
<table class="table table-striped table-hover table-responsive table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a href="#IDoNotExist" ng-click="sortType = 'username'; sortReverse = !sortReverse">
|
||||
Username
|
||||
<span ng-show="sortType == 'username' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'username' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="#IDoNotExist" ng-click="sortType = 'alias'; sortReverse = !sortReverse">
|
||||
Alias
|
||||
<span ng-show="sortType == 'alias' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'alias' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="#IDoNotExist" ng-click="sortType = 'emailAddress'; sortReverse = !sortReverse">
|
||||
Email
|
||||
<span ng-show="sortType == 'emailAddress' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'emailAddress' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
Permissions
|
||||
</th>
|
||||
<th ng-hide="hideColumns">
|
||||
<a href="#IDoNotExist" ng-click="sortType = 'type'; sortReverse = !sortReverse">
|
||||
User Type
|
||||
<span ng-show="sortType == 'type' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'type' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<th ng-hide="hideColumns">
|
||||
<a href="#IDoNotExist" ng-click="sortType = 'lastLoggedIn'; sortReverse = !sortReverse">
|
||||
Last Logged In
|
||||
<span ng-show="sortType == 'lastLoggedIn' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'lastLoggedIn' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="u in users | orderBy:sortType:sortReverse | filter:searchTerm">
|
||||
<td>
|
||||
{{u.username}}
|
||||
</td>
|
||||
<td>
|
||||
{{u.alias}}
|
||||
</td>
|
||||
<td>
|
||||
{{u.emailAddress}}
|
||||
</td>
|
||||
<td>
|
||||
{{u.permissionsFormattedString}}
|
||||
</td>
|
||||
<td ng-hide="hideColumns">
|
||||
{{u.type === 1 ? 'Local User' : 'Plex User'}}
|
||||
</td>
|
||||
<td ng-hide="hideColumns" ng-bind="u.lastLoggedIn === minDate ? 'Never' : formatDate(u.lastLoggedIn)"></td>
|
||||
<td>
|
||||
<a href="#IDontExist" ng-click="selectUser(u.id)" class="btn btn-sm btn-info-outline">Details/Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,25 @@
|
|||
(function () {
|
||||
|
||||
module.directive('tableComponent',
|
||||
function () {
|
||||
return {
|
||||
templateUrl: createBaseUrl(getBaseUrl(), 'Content/app/userManagement/Directives/table.html')
|
||||
};
|
||||
})
|
||||
.directive('addUser',
|
||||
function () {
|
||||
return {
|
||||
templateUrl: createBaseUrl(getBaseUrl(), 'Content/app/userManagement/Directives/addUser.html')
|
||||
};
|
||||
})
|
||||
.directive('sidebar',
|
||||
function () {
|
||||
return {
|
||||
templateUrl: createBaseUrl(getBaseUrl(), 'Content/app/userManagement/Directives/sidebar.html')
|
||||
};
|
||||
});
|
||||
|
||||
function getBaseUrl() {
|
||||
return $('#baseUrl').text();
|
||||
}
|
||||
})();
|
|
@ -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]);
|
||||
}());
|
|
@ -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]);
|
||||
|
||||
|
|
75
PlexRequests.UI/Content/base.css
vendored
75
PlexRequests.UI/Content/base.css
vendored
|
@ -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; }
|
||||
|
||||
|
|
2
PlexRequests.UI/Content/base.min.css
vendored
2
PlexRequests.UI/Content/base.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||
}
|
||||
|
|
7
PlexRequests.UI/Content/clipboard.min.js
vendored
Normal file
7
PlexRequests.UI/Content/clipboard.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16
PlexRequests.UI/Content/requests.js
vendored
16
PlexRequests.UI/Content/requests.js
vendored
|
@ -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');
|
||||
|
|
41
PlexRequests.UI/Content/search.js
vendored
41
PlexRequests.UI/Content/search.js
vendored
|
@ -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
2
PlexRequests.UI/Content/spin.min.js
vendored
Normal 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});
|
25
PlexRequests.UI/Content/wizard.js
vendored
25
PlexRequests.UI/Content/wizard.js
vendored
|
@ -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');
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
75
PlexRequests.UI/Modules/Admin/FaultQueueModule.cs
Normal file
75
PlexRequests.UI/Modules/Admin/FaultQueueModule.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
133
PlexRequests.UI/Modules/Admin/SystemStatusModule.cs
Normal file
133
PlexRequests.UI/Modules/Admin/SystemStatusModule.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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." });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" });
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
18
PlexRequests.UI/Resources/UI1.Designer.cs
generated
18
PlexRequests.UI/Resources/UI1.Designer.cs
generated
|
@ -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'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>
|
||||
|
|
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@using PlexRequests.UI.Helpers
|
||||
@Html.Partial("_Sidebar")
|
||||
@Html.Partial("Shared/Partial/_Sidebar")
|
||||
@{
|
||||
int port;
|
||||
if (Model.Port == 0)
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@using PlexRequests.UI.Helpers
|
||||
@Html.Partial("_Sidebar")
|
||||
@Html.Partial("Shared/Partial/_Sidebar")
|
||||
@{
|
||||
int port;
|
||||
if (Model.Port == 0)
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@using PlexRequests.UI.Helpers
|
||||
@Html.Partial("_Sidebar")
|
||||
@Html.Partial("Shared/Partial/_Sidebar")
|
||||
@{
|
||||
int port;
|
||||
if (Model.Port == 0)
|
||||
|
|
|
@ -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>
|
109
PlexRequests.UI/Views/FaultQueue/RequestFaultQueue.cshtml
Normal file
109
PlexRequests.UI/Views/FaultQueue/RequestFaultQueue.cshtml
Normal 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>*@
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue