Merge pull request #231 from tidusjar/dev

Release 1.7
pull/232/head
Jamie 9 years ago
commit 6249a503f3

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexRegistry.cs
// File: ISlackApi.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -24,18 +24,14 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Threading.Tasks;
using FluentScheduler;
using PlexRequests.Api.Models.Notifications;
using PlexRequests.Services;
namespace PlexRequests.UI.Jobs
namespace PlexRequests.Api.Interfaces
{
public class PlexRegistry : Registry
public interface ISlackApi
{
public PlexRegistry()
{
Schedule<AvailabilityUpdateService>().ToRunNow();
}
Task<string> PushAsync(string team, string token, string service, SlackNotificationBody message);
}
}

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PlexRequests.Api.Interfaces</RootNamespace>
<AssemblyName>PlexRequests.Api.Interfaces</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
@ -32,7 +32,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="RestSharp, Version=105.2.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\RestSharp.105.2.3\lib\net452\RestSharp.dll</HintPath>
<HintPath>..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
@ -51,20 +51,21 @@
<Compile Include="IMusicBrainzApi.cs" />
<Compile Include="IPlexApi.cs" />
<Compile Include="IPushbulletApi.cs" />
<Compile Include="ISlackApi.cs" />
<Compile Include="IPushoverApi.cs" />
<Compile Include="ISickRageApi.cs" />
<Compile Include="ISonarrApi.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PlexRequests.Api.Models\PlexRequests.Api.Models.csproj">
<Project>{CB37A5F8-6DFC-4554-99D3-A42B502E4591}</Project>
<Name>PlexRequests.Api.Models</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="RestSharp" version="105.2.3" targetFramework="net452" />
<package id="RestSharp" version="105.2.3" targetFramework="net45" />
</packages>

@ -0,0 +1,56 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SlackNotificationBody.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 Newtonsoft.Json;
namespace PlexRequests.Api.Models.Notifications
{
public class SlackNotificationBody
{
[JsonConstructor]
public SlackNotificationBody()
{
username = "Plex Requests";
}
[JsonIgnore]
private string _username;
public string username
{
get { return _username; }
set
{
if (!string.IsNullOrEmpty(value))
_username = value;
}
}
public string channel { get; set; }
public string text { get; set; }
public string icon_url { get; set; }
public string icon_emoji { get; set; }
}
}

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PlexRequests.Api.Models</RootNamespace>
<AssemblyName>PlexRequests.Api.Models</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
@ -31,10 +31,6 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
@ -43,6 +39,9 @@
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Movie\CouchPotatoAdd.cs" />
@ -59,6 +58,7 @@
<Compile Include="Notifications\PushbulletPush.cs" />
<Compile Include="Notifications\PushbulletResponse.cs" />
<Compile Include="Notifications\PushoverResponse.cs" />
<Compile Include="Notifications\SlackNotificationBody.cs" />
<Compile Include="Plex\PlexAccount.cs" />
<Compile Include="Plex\PlexAuthentication.cs" />
<Compile Include="Plex\PlexError.cs" />

@ -24,5 +24,56 @@ namespace PlexRequests.Api.Models.Tv
public int updated { get; set; }
public Links _links { get; set; }
public int seasonCount { get; set; }
public Embedded _embedded { get; set; }
}
public class Season
{
public int id { get; set; }
public string url { get; set; }
public int number { get; set; }
public string name { get; set; }
public int? episodeOrder { get; set; }
public string premiereDate { get; set; }
public string endDate { get; set; }
public Network2 network { get; set; }
public object webChannel { get; set; }
public Image2 image { get; set; }
public string summary { get; set; }
public Links2 _links { get; set; }
}
public class Country2
{
public string name { get; set; }
public string code { get; set; }
public string timezone { get; set; }
}
public class Network2
{
public int id { get; set; }
public string name { get; set; }
public Country2 country { get; set; }
}
public class Image2
{
public string medium { get; set; }
public string original { get; set; }
}
public class Self2
{
public string href { get; set; }
}
public class Links2
{
public Self2 self { get; set; }
}
public class Embedded
{
public List<Season> seasons { get; set; }
}
}

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net46" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net45" />
</packages>

@ -25,7 +25,6 @@
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
@ -34,14 +33,15 @@ using Newtonsoft.Json;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Exceptions;
using RestSharp;
namespace PlexRequests.Api
{
public class ApiRequest : IApiRequest
{
private JsonSerializerSettings Settings = new JsonSerializerSettings
private readonly JsonSerializerSettings _settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Ignore
@ -66,7 +66,8 @@ namespace PlexRequests.Api
if (response.ErrorException != null)
{
var message = "Error retrieving response. Check inner details for more info.";
throw new ApplicationException(message, response.ErrorException);
Log.Error(response.ErrorException);
throw new ApiRequestException(message, response.ErrorException);
}
return response.Data;
@ -80,8 +81,9 @@ namespace PlexRequests.Api
if (response.ErrorException != null)
{
Log.Error(response.ErrorException);
var message = "Error retrieving response. Check inner details for more info.";
throw new ApplicationException(message, response.ErrorException);
throw new ApiRequestException(message, response.ErrorException);
}
return response;
@ -95,8 +97,9 @@ namespace PlexRequests.Api
if (response.ErrorException != null)
{
Log.Error(response.ErrorException);
var message = "Error retrieving response. Check inner details for more info.";
throw new ApplicationException(message, response.ErrorException);
throw new ApiRequestException(message, response.ErrorException);
}
var result = DeserializeXml<T>(response.Content);
@ -106,18 +109,18 @@ namespace PlexRequests.Api
public T ExecuteJson<T>(IRestRequest request, Uri baseUri) where T : new()
{
var client = new RestClient { BaseUrl = baseUri };
var response = client.Execute(request);
Log.Trace("Api Content Response:");
Log.Trace(response.Content);
if (response.ErrorException != null)
{
Log.Error(response.ErrorException);
var message = "Error retrieving response. Check inner details for more info.";
throw new ApplicationException(message, response.ErrorException);
throw new ApiRequestException(message, response.ErrorException);
}
Log.Trace("Deserialzing Object");
var json = JsonConvert.DeserializeObject<T>(response.Content, Settings);
var json = JsonConvert.DeserializeObject<T>(response.Content, _settings);
Log.Trace("Finished Deserialzing Object");
return json;
@ -133,8 +136,9 @@ namespace PlexRequests.Api
using (var sr = new StringReader(input))
return (T)ser.Deserialize(sr);
}
catch (InvalidOperationException)
catch (InvalidOperationException e)
{
Log.Error(e);
return null;
}
}

@ -31,6 +31,7 @@ using Newtonsoft.Json.Linq;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Movie;
using PlexRequests.Helpers.Exceptions;
using RestSharp;
@ -61,7 +62,12 @@ namespace PlexRequests.Api
request.AddUrlSegment("imdbid", imdbid);
request.AddUrlSegment("title", title);
var obj = Api.ExecuteJson<JObject>(request, baseUrl);
var obj = RetryHandler.Execute<JObject>(() => Api.ExecuteJson<JObject> (request, baseUrl),new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)},
(exception, timespan) => Log.Error (exception, "Exception when calling AddMovie for CP, Retrying {0}", timespan));
Log.Trace("CP movie Add result count {0}", obj.Count);
if (obj.Count > 0)
@ -99,7 +105,14 @@ namespace PlexRequests.Api
request.AddUrlSegment("apikey", apiKey);
return Api.Execute<CouchPotatoStatus>(request,url);
var obj = RetryHandler.Execute<CouchPotatoStatus>(() => Api.Execute<CouchPotatoStatus> (request, url),new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)},
(exception, timespan) => Log.Error (exception, "Exception when calling GetStatus for CP, Retrying {0}", timespan));
return obj;
}
public CouchPotatoProfiles GetProfiles(Uri url, string apiKey)
@ -113,18 +126,39 @@ namespace PlexRequests.Api
request.AddUrlSegment("apikey", apiKey);
return Api.Execute<CouchPotatoProfiles>(request, url);
var obj = RetryHandler.Execute<CouchPotatoProfiles>(() => Api.Execute<CouchPotatoProfiles> (request, url),null,
(exception, timespan) => Log.Error (exception, "Exception when calling GetProfiles for CP, Retrying {0}", timespan));
return obj;
}
public CouchPotatoMovies GetMovies(Uri baseUrl, string apiKey, string[] status)
{
RestRequest request;
request = new RestRequest { Resource = "/api/{apikey}/movie.list?status={status}" };
var request = new RestRequest
{
Resource = "/api/{apikey}/movie.list?status={status}"
};
request.AddUrlSegment("apikey", apiKey);
request.AddUrlSegment("status", string.Join(",", status));
return Api.Execute<CouchPotatoMovies>(request, baseUrl);
try
{
var obj = RetryHandler.Execute<CouchPotatoMovies>(() => Api.Execute<CouchPotatoMovies> (request, baseUrl),
new TimeSpan[] {
TimeSpan.FromSeconds (5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
},
(exception, timespan) => Log.Error (exception, "Exception when calling GetMovies for CP, Retrying {0}", timespan));
return obj;
}
catch (Exception e) // Request error is already logged in the ApiRequest class
{
Log.Error("Error when attempting to GetMovies.");
Log.Error (e);
return new CouchPotatoMovies();
}
}
}
}

@ -71,7 +71,7 @@ namespace PlexRequests.Api
return albumResult;
}
catch (JsonSerializationException jse)
catch (Exception jse)
{
Log.Error(jse);
return false; // If there is no matching result we do not get returned a JSON string, it just returns "false".
@ -94,7 +94,7 @@ namespace PlexRequests.Api
return result;
}
catch (JsonSerializationException jse)
catch (Exception jse)
{
Log.Error(jse);
return new List<HeadphonesGetIndex>();

@ -23,17 +23,20 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
using Polly;
#endregion
using System;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Exceptions;
using RestSharp;
using System.Xml;
using System.Collections.Generic;
namespace PlexRequests.Api
{
@ -43,6 +46,19 @@ namespace PlexRequests.Api
{
Version = AssemblyHelper.GetAssemblyVersion();
}
public PlexApi (IApiRequest api)
{
Api = api;
}
private IApiRequest Api { get; }
private const string SignInUri = "https://plex.tv/users/sign_in.json";
private const string FriendsUri = "https://plex.tv/pms/friends/all";
private const string GetAccountUri = "https://plex.tv/users/account";
private static Logger Log = LogManager.GetCurrentClassLogger();
private static string Version { get; }
public PlexAuthentication SignIn(string username, string password)
@ -64,8 +80,11 @@ namespace PlexRequests.Api
request.AddJsonBody(userModel);
var api = new ApiRequest();
return api.Execute<PlexAuthentication>(request, new Uri("https://plex.tv/users/sign_in.json"));
var obj = RetryHandler.Execute<PlexAuthentication>(() => Api.Execute<PlexAuthentication> (request, new Uri(SignInUri)),
null,
(exception, timespan) => Log.Error (exception, "Exception when calling SignIn for Plex, Retrying {0}", timespan));
return obj;
}
public PlexFriends GetUsers(string authToken)
@ -77,8 +96,10 @@ namespace PlexRequests.Api
AddHeaders(ref request, authToken);
var api = new ApiRequest();
var users = api.ExecuteXml<PlexFriends>(request, new Uri("https://plex.tv/pms/friends/all"));
var users = RetryHandler.Execute(() => Api.ExecuteXml<PlexFriends> (request, new Uri(FriendsUri)),
null,
(exception, timespan) => Log.Error (exception, "Exception when calling GetUsers for Plex, Retrying {0}", timespan));
return users;
}
@ -101,8 +122,9 @@ namespace PlexRequests.Api
request.AddUrlSegment("searchTerm", searchTerm);
AddHeaders(ref request, authToken);
var api = new ApiRequest();
var search = api.ExecuteXml<PlexSearch>(request, plexFullHost);
var search = RetryHandler.Execute<PlexSearch>(() => Api.ExecuteXml<PlexSearch> (request, plexFullHost),
null,
(exception, timespan) => Log.Error (exception, "Exception when calling SearchContent for Plex, Retrying {0}", timespan));
return search;
}
@ -116,8 +138,9 @@ namespace PlexRequests.Api
AddHeaders(ref request, authToken);
var api = new ApiRequest();
var users = api.ExecuteXml<PlexStatus>(request, uri);
var users = RetryHandler.Execute<PlexStatus>(() => Api.ExecuteXml<PlexStatus> (request, uri),
null,
(exception, timespan) => Log.Error (exception, "Exception when calling GetStatus for Plex, Retrying {0}", timespan));
return users;
}
@ -131,8 +154,9 @@ namespace PlexRequests.Api
AddHeaders(ref request, authToken);
var api = new ApiRequest();
var account = api.ExecuteXml<PlexAccount>(request, new Uri("https://plex.tv/users/account"));
var account = RetryHandler.Execute<PlexAccount>(() => Api.ExecuteXml<PlexAccount> (request, new Uri(GetAccountUri)),
null,
(exception, timespan) => Log.Error (exception, "Exception when calling GetAccount for Plex, Retrying {0}", timespan));
return account;
}
@ -147,10 +171,23 @@ namespace PlexRequests.Api
AddHeaders(ref request, authToken);
var api = new ApiRequest();
var sections = api.ExecuteXml<PlexLibraries>(request, plexFullHost);
return sections;
try
{
var lib = RetryHandler.Execute<PlexLibraries>(() => Api.ExecuteXml<PlexLibraries> (request, plexFullHost),
new TimeSpan[] {
TimeSpan.FromSeconds (5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
},
(exception, timespan) => Log.Error (exception, "Exception when calling GetLibrarySections for Plex, Retrying {0}", timespan));
return lib;
}
catch (Exception e)
{
Log.Error(e,"There has been a API Exception when attempting to get the Plex Libraries");
return new PlexLibraries();
}
}
public PlexSearch GetLibrary(string authToken, Uri plexFullHost, string libraryId)
@ -161,13 +198,26 @@ namespace PlexRequests.Api
Resource = "library/sections/{libraryId}/all"
};
request.AddUrlSegment("libraryId", libraryId.ToString());
request.AddUrlSegment("libraryId", libraryId);
AddHeaders(ref request, authToken);
var api = new ApiRequest();
var search = api.ExecuteXml<PlexSearch>(request, plexFullHost);
return search;
try
{
var lib = RetryHandler.Execute<PlexSearch>(() => Api.ExecuteXml<PlexSearch> (request, plexFullHost),
new TimeSpan[] {
TimeSpan.FromSeconds (5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
},
(exception, timespan) => Log.Error (exception, "Exception when calling GetLibrary for Plex, Retrying {0}", timespan));
return lib;
}
catch (Exception e)
{
Log.Error(e,"There has been a API Exception when attempting to get the Plex Library");
return new PlexSearch();
}
}
private void AddHeaders(ref RestRequest request, string authToken)

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PlexRequests.Api</RootNamespace>
<AssemblyName>PlexRequests.Api</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
@ -31,24 +31,8 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dapper, Version=1.40.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Dapper.1.42\lib\net45\Dapper.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="RestSharp, Version=105.2.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\RestSharp.105.2.3\lib\net452\RestSharp.dll</HintPath>
<HintPath>..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
@ -59,9 +43,23 @@
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="TMDbLib, Version=0.9.0.0, Culture=neutral, processorArchitecture=MSIL">
<Reference Include="Dapper, Version=1.40.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\Dapper.1.42\lib\net45\Dapper.dll</HintPath>
</Reference>
<Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
</Reference>
<Reference Include="TMDbLib, Version=0.9.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Polly">
<HintPath>..\packages\Polly-Signed.4.2.0\lib\net45\Polly.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
@ -73,6 +71,7 @@
<DependentUpon>MockApiData.resx</DependentUpon>
</Compile>
<Compile Include="Mocks\MockSonarrApi.cs" />
<Compile Include="SlackApi.cs" />
<Compile Include="PushoverApi.cs" />
<Compile Include="PushbulletApi.cs" />
<Compile Include="SickrageApi.cs" />
@ -87,6 +86,7 @@
<Compile Include="TheTvDbApi.cs" />
<Compile Include="TvMazeApi.cs" />
<Compile Include="TvMazeBase.cs" />
<Compile Include="RetryHandler.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />

@ -0,0 +1,71 @@
using System;
using Polly.Retry;
using Polly;
using System.Threading.Tasks;
namespace PlexRequests.Api
{
public static class RetryHandler
{
private static readonly TimeSpan[] DefaultTime = new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)};
public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] timeSpan, Action action)
{
if(timeSpan == null)
{
timeSpan = DefaultTime;
}
var policy = Policy.Handle<Exception> ()
.WaitAndRetry(timeSpan, (e, ts) => action());
return policy;
}
public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] timeSpan)
{
if(timeSpan == null)
{
timeSpan = DefaultTime;
}
var policy = Policy.Handle<Exception> ()
.WaitAndRetry(timeSpan);
return policy;
}
public static RetryPolicy RetryAndWaitPolicy(TimeSpan[] timeSpan, Action<Exception, TimeSpan> action)
{
if(timeSpan == null)
{
timeSpan = DefaultTime;
}
var policy = Policy.Handle<Exception> ()
.WaitAndRetry(timeSpan, action);
return policy;
}
public static T Execute<T>(Func<T> action, TimeSpan[] timeSpan)
{
var policy = RetryAndWaitPolicy (timeSpan);
return policy.Execute (action);
}
public static T Execute<T>(Func<T> func, TimeSpan[] timeSpan, Action<Exception, TimeSpan> action)
{
if(timeSpan == null)
{
timeSpan = DefaultTime;
}
var policy = RetryAndWaitPolicy (timeSpan, action);
return policy.Execute (func);
}
}
}

@ -1,8 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: CouchPotatoApi.cs
// File: SickrageApi.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -24,28 +23,28 @@
// 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;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.SickRage;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Exceptions;
using RestSharp;
using Newtonsoft.Json.Linq;
namespace PlexRequests.Api
{
public class SickrageApi : ISickRageApi
{
private static Logger Log = LogManager.GetCurrentClassLogger();
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public SickrageApi()
{
@ -54,22 +53,38 @@ namespace PlexRequests.Api
private ApiRequest Api { get; }
public async Task<SickRageTvAdd> AddSeries(int tvdbId, int seasonCount, int[] seasons, string quality, string apiKey,
Uri baseUrl)
public SickRageSeasonList VerifyShowHasLoaded(int tvdbId, string apiKey, Uri baseUrl)
{
Log.Trace("Entered `VerifyShowHasLoaded({0} <- id)`", tvdbId);
var request = new RestRequest { Resource = "/api/{apiKey}/?cmd=show.seasonlist", Method = Method.GET };
request.AddUrlSegment("apiKey", apiKey);
request.AddQueryParameter("tvdbid", tvdbId.ToString());
try
{
var policy = RetryHandler.RetryAndWaitPolicy(
null,
(exception, timespan) => Log.Error(exception, "Exception when calling VerifyShowHasLoaded for SR, Retrying {0}", timespan));
var obj = policy.Execute(() => Api.ExecuteJson<SickRageSeasonList>(request, baseUrl));
return obj;
}
catch (Exception e)
{
Log.Error(e);
return new SickRageSeasonList();
}
}
public async Task<SickRageTvAdd> AddSeries(int tvdbId, int seasonCount, int[] seasons, string quality, string apiKey, Uri baseUrl)
{
var futureStatus = seasons.Length > 0 && !seasons.Any(x => x == seasonCount) ? SickRageStatus.Skipped : SickRageStatus.Wanted;
var status = seasons.Length > 0 ? SickRageStatus.Skipped : SickRageStatus.Wanted;
Log.Trace("Future Status: {0}", futureStatus);
Log.Trace("Current Status: {0}", status);
var request = new RestRequest
{
Resource = "/api/{apiKey}/?cmd=show.addnew",
Method = Method.GET
};
var request = new RestRequest { Resource = "/api/{apiKey}/?cmd=show.addnew", Method = Method.GET };
request.AddUrlSegment("apiKey", apiKey);
request.AddQueryParameter("tvdbid", tvdbId.ToString());
request.AddQueryParameter("status", status);
@ -80,9 +95,11 @@ namespace PlexRequests.Api
request.AddQueryParameter("initial", quality);
}
Log.Trace("Entering `Execute<SickRageTvAdd>`");
var obj = Api.Execute<SickRageTvAdd>(request, baseUrl);
Log.Trace("Exiting `Execute<SickRageTvAdd>`");
var policy = RetryHandler.RetryAndWaitPolicy(
null,
(exception, timespan) => Log.Error(exception, "Exception when calling AddSeries for SR, Retrying {0}", timespan));
var obj = policy.Execute(() => Api.Execute<SickRageTvAdd>(request, baseUrl));
Log.Trace("obj Result:");
Log.Trace(obj.DumpJson());
@ -93,7 +110,6 @@ namespace PlexRequests.Api
var seasonIncrement = 0;
var seasonList = new SickRageSeasonList();
Log.Trace("while (seasonIncrement < seasonCount) where seasonCount = {0}", seasonCount);
try
{
while (seasonIncrement < seasonCount)
@ -130,6 +146,7 @@ namespace PlexRequests.Api
foreach (var s in seasons)
{
Log.Trace("Adding season {0}", s);
var result = await AddSeason(tvdbId, s, apiKey, baseUrl);
Log.Trace("SickRage adding season results: ");
Log.Trace(result.DumpJson());
@ -149,76 +166,62 @@ namespace PlexRequests.Api
public SickRagePing Ping(string apiKey, Uri baseUrl)
{
var request = new RestRequest
{
Resource = "/api/{apiKey}/?cmd=sb.ping",
Method = Method.GET
};
var request = new RestRequest { Resource = "/api/{apiKey}/?cmd=sb.ping", Method = Method.GET };
request.AddUrlSegment("apiKey", apiKey);
var obj = Api.ExecuteJson<SickRagePing>(request, baseUrl);
var policy = RetryHandler.RetryAndWaitPolicy(
null,
(exception, timespan) => Log.Error(exception, "Exception when calling Ping for SR, Retrying {0}", timespan));
return obj;
}
public SickRageSeasonList VerifyShowHasLoaded(int tvdbId, string apiKey, Uri baseUrl)
{
Log.Trace("Entered `VerifyShowHasLoaded({0} <- id)`", tvdbId);
var request = new RestRequest
{
Resource = "/api/{apiKey}/?cmd=show.seasonlist",
Method = Method.GET
};
request.AddUrlSegment("apiKey", apiKey);
request.AddQueryParameter("tvdbid", tvdbId.ToString());
var obj = policy.Execute(() => Api.ExecuteJson<SickRagePing>(request, baseUrl));
try
{
Log.Trace("Entering `ExecuteJson<SickRageSeasonList>`");
var obj = Api.ExecuteJson<SickRageSeasonList>(request, baseUrl);
Log.Trace("Exited `ExecuteJson<SickRageSeasonList>`");
return obj;
}
catch (Exception e)
{
Log.Error(e);
return new SickRageSeasonList();
}
return obj;
}
public async Task<SickRageTvAdd> AddSeason(int tvdbId, int season, string apiKey, Uri baseUrl)
{
var request = new RestRequest
{
Resource = "/api/{apiKey}/?cmd=episode.setstatus",
Method = Method.GET
};
var request = new RestRequest { Resource = "/api/{apiKey}/?cmd=episode.setstatus", Method = Method.GET };
request.AddUrlSegment("apiKey", apiKey);
request.AddQueryParameter("tvdbid", tvdbId.ToString());
request.AddQueryParameter("season", season.ToString());
request.AddQueryParameter("status", SickRageStatus.Wanted);
await Task.Run(() => Thread.Sleep(2000));
return await Task.Run(() =>
{
Log.Trace("Entering `Execute<SickRageTvAdd>` in a new `Task<T>`");
var result = Api.Execute<SickRageTvAdd>(request, baseUrl);
return await Task.Run(
() =>
{
var policy = RetryHandler.RetryAndWaitPolicy(
null,
(exception, timespan) => Log.Error(exception, "Exception when calling AddSeason for SR, Retrying {0}", timespan));
var result = policy.Execute(() => Api.Execute<SickRageTvAdd>(request, baseUrl));
Log.Trace("Exiting `Execute<SickRageTvAdd>` and yeilding `Task<T>` result");
return result;
}).ConfigureAwait(false);
return result;
}).ConfigureAwait(false);
}
public async Task<SickrageShows> GetShows(string apiKey, Uri baseUrl)
{
var request = new RestRequest
{
Resource = "/api/{apiKey}/?cmd=shows",
Method = Method.GET
};
var request = new RestRequest { Resource = "/api/{apiKey}/?cmd=shows", Method = Method.GET };
request.AddUrlSegment("apiKey", apiKey);
return await Task.Run(() => Api.Execute<SickrageShows>(request, baseUrl)).ConfigureAwait(false);
return await Task.Run(
() =>
{
try
{
var policy = RetryHandler.RetryAndWaitPolicy(
new[] { TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30) },
(exception, timespan) => Log.Error(exception, "Exception when calling GetShows for SR, Retrying {0}", timespan));
return policy.Execute(() => Api.Execute<SickrageShows>(request, baseUrl));
}
catch (ApiRequestException)
{
Log.Error("There has been a API exception when Getting the Sickrage shows");
return null;
}
}).ConfigureAwait(false);
}
}
}

@ -0,0 +1,62 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexApi.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.Threading.Tasks;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Notifications;
using RestSharp;
namespace PlexRequests.Api
{
public class SlackApi : ISlackApi
{
public async Task<string> PushAsync(string team, string token, string service, SlackNotificationBody message)
{
var request = new RestRequest
{
Method = Method.POST,
Resource = "/services/{team}/{service}/{token}"
};
request.AddUrlSegment("team", team);
request.AddUrlSegment("service", service);
request.AddUrlSegment("token", token);
request.AddJsonBody(message);
var api = new ApiRequest();
return await Task.Run(
() =>
{
var result = api.Execute(request, new Uri("https://hooks.slack.com/"));
return result.Content;
});
}
}
}

@ -38,6 +38,8 @@ using PlexRequests.Helpers;
using RestSharp;
using Newtonsoft.Json.Linq;
using PlexRequests.Helpers.Exceptions;
namespace PlexRequests.Api
{
public class SonarrApi : ISonarrApi
@ -54,8 +56,13 @@ namespace PlexRequests.Api
var request = new RestRequest { Resource = "/api/profile", Method = Method.GET };
request.AddHeader("X-Api-Key", apiKey);
var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}, (exception, timespan) => Log.Error (exception, "Exception when calling GetProfiles for Sonarr, Retrying {0}", timespan));
var obj = Api.ExecuteJson<List<SonarrProfile>>(request, baseUrl);
var obj = policy.Execute(() => Api.ExecuteJson<List<SonarrProfile>>(request, baseUrl));
return obj;
}
@ -100,7 +107,13 @@ namespace PlexRequests.Api
SonarrAddSeries result;
try
{
result = Api.ExecuteJson<SonarrAddSeries>(request, baseUrl);
var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}, (exception, timespan) => Log.Error (exception, "Exception when calling AddSeries for Sonarr, Retrying {0}", timespan));
result = policy.Execute(() => Api.ExecuteJson<SonarrAddSeries>(request, baseUrl));
}
catch (JsonSerializationException jse)
{
@ -119,7 +132,13 @@ namespace PlexRequests.Api
var request = new RestRequest { Resource = "/api/system/status", Method = Method.GET };
request.AddHeader("X-Api-Key", apiKey);
var obj = Api.ExecuteJson<SystemStatus>(request, baseUrl);
var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}, (exception, timespan) => Log.Error (exception, "Exception when calling SystemStatus for Sonarr, Retrying {0}", timespan));
var obj = policy.Execute(() => Api.ExecuteJson<SystemStatus>(request, baseUrl));
return obj;
}
@ -128,8 +147,21 @@ namespace PlexRequests.Api
{
var request = new RestRequest { Resource = "/api/series", Method = Method.GET };
request.AddHeader("X-Api-Key", apiKey);
return Api.Execute<List<Series>>(request, baseUrl);
try
{
var policy = RetryHandler.RetryAndWaitPolicy (new TimeSpan[] {
TimeSpan.FromSeconds (5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
}, (exception, timespan) => Log.Error (exception, "Exception when calling GetSeries for Sonarr, Retrying {0}", timespan));
return policy.Execute(() => Api.ExecuteJson<List<Series>>(request, baseUrl));
}
catch (Exception e)
{
Log.Error(e, "There has been an API exception when getting the Sonarr Series");
return null;
}
}
}
}

@ -50,7 +50,7 @@ namespace PlexRequests.Api
return results.Results;
}
[Obsolete("Should use TheTvDbApi for TV")]
[Obsolete("Should use TvMaze for TV")]
public async Task<List<SearchTv>> SearchTv(string searchTerm)
{
var results = await Client.SearchTvShow(searchTerm);
@ -74,7 +74,7 @@ namespace PlexRequests.Api
return movies;
}
[Obsolete("Should use TheTvDbApi for TV")]
[Obsolete("Should use TvMaze for TV")]
public async Task<TvShow> GetTvShowInformation(int tmdbId)
{
var show = await Client.GetTvShow(tmdbId);

@ -24,12 +24,15 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using PlexRequests.Api.Models.Tv;
using RestSharp;
namespace PlexRequests.Api
{
[Obsolete("Use TVMazeAPP")]
public class TheTvDbApi : TvBase
{
public TheTvDbApi()

@ -85,7 +85,7 @@ namespace PlexRequests.Api
return obj;
}
public int GetSeasonCount(int id)
public List<TvMazeSeasons> GetSeasons(int id)
{
var request = new RestRequest
{
@ -95,7 +95,11 @@ namespace PlexRequests.Api
request.AddUrlSegment("id", id.ToString());
request.AddHeader("Content-Type", "application/json");
var obj = Api.Execute<List<TvMazeSeasons>>(request, new Uri(Uri));
return Api.Execute<List<TvMazeSeasons>>(request, new Uri(Uri));
}
public int GetSeasonCount(int id)
{
var obj = GetSeasons(id);
var seasons = obj.Select(x => x.number > 0);
return seasons.Count();
}

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /></startup></configuration>

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Dapper" version="1.42" targetFramework="net452" />
<package id="Nancy" version="1.4.3" targetFramework="net452" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net452" />
<package id="NLog" version="4.2.3" targetFramework="net452" />
<package id="RestSharp" version="105.2.3" targetFramework="net452" />
<package id="TMDbLib" version="0.9.0.0-alpha" targetFramework="net452" />
<package id="Dapper" version="1.42" targetFramework="net45" />
<package id="Nancy" version="1.4.3" targetFramework="net45" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net45" />
<package id="NLog" version="4.2.3" targetFramework="net45" />
<package id="Polly-Signed" version="4.2.0" targetFramework="net45" />
<package id="RestSharp" version="105.2.3" targetFramework="net45" />
<package id="TMDbLib" version="0.9.0.0-alpha" targetFramework="net45" />
</packages>

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /></startup></configuration>

@ -28,6 +28,11 @@ namespace PlexRequests.Core
{
public class CacheKeys
{
public struct TimeFrameMinutes
{
public const int SchedulerCaching = 60;
}
public const string PlexLibaries = "PlexLibaries";
public const string TvDbToken = "TheTvDbApiToken";
@ -41,6 +46,8 @@ namespace PlexRequests.Core
public const string CouchPotatoQualityProfiles = "CouchPotatoQualityProfiles";
public const string CouchPotatoQueued = "CouchPotatoQueued";
public const string GetBaseUrl = "GetBaseUrl";
public const string GetPlexRequestSettings = "GetPlexRequestSettings";
public const string LastestProductVersion = "LatestProductVersion";
}
}

@ -49,10 +49,9 @@ namespace PlexRequests.Core
var entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId };
var id = Repo.Insert(entity);
// TODO Keep an eye on this, since we are now doing 2 DB update for 1 single request, inserting and then updating
model.Id = (int)id;
entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id, MusicId = model.MusicBrainzId};
entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id, MusicId = model.MusicBrainzId };
var result = Repo.Update(entity);
return result ? id : -1;

@ -24,21 +24,11 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Text.RegularExpressions;
namespace PlexRequests.Core.Models
{
public class StatusModel
{
public string Version { get; set; }
public int DBVersion {
get
{
string trimStatus = new Regex("[^0-9]", RegexOptions.Compiled).Replace(Version, string.Empty).PadRight(4, '0');
return int.Parse(trimStatus);
}
}
public bool UpdateAvailable { get; set; }
public string UpdateUri { get; set; }
public string DownloadUri { get; set; }

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PlexRequests.Core</RootNamespace>
<AssemblyName>PlexRequests.Core</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
@ -34,40 +34,34 @@
<Reference Include="Mono.Data.Sqlite">
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</Reference>
<Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="TMDbLib">
<HintPath>..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll</HintPath>
</Reference>
<Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Nancy.Authentication.Forms, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL">
<Reference Include="Nancy.Authentication.Forms, Version=1.4.1.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\Nancy.Authentication.Forms.1.4.1\lib\net40\Nancy.Authentication.Forms.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Octokit, Version=0.19.0.0, Culture=neutral, processorArchitecture=MSIL">
<Reference Include="Octokit, Version=0.19.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\Octokit.0.19.0\lib\net45\Octokit.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Omu.ValueInjecter, Version=3.1.1.0, Culture=neutral, PublicKeyToken=c7694541b0ac80e4, processorArchitecture=MSIL">
<Reference Include="Omu.ValueInjecter, Version=3.1.1.0, Culture=neutral, PublicKeyToken=c7694541b0ac80e4">
<HintPath>..\packages\valueinjecter.3.1.1.2\lib\net40\Omu.ValueInjecter.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="TMDbLib">
<HintPath>..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
@ -79,10 +73,12 @@
<Compile Include="Models\UserProperties.cs" />
<Compile Include="SettingModels\AuthenticationSettings.cs" />
<Compile Include="SettingModels\HeadphonesSettings.cs" />
<Compile Include="SettingModels\SlackNotificationSettings.cs" />
<Compile Include="SettingModels\PushoverNotificationSettings.cs" />
<Compile Include="SettingModels\PushBulletNotificationSettings.cs" />
<Compile Include="SettingModels\EmailNotificationSettings.cs" />
<Compile Include="SettingModels\PlexSettings.cs" />
<Compile Include="SettingModels\LogSettings.cs" />
<Compile Include="SettingModels\SonarrSettings.cs" />
<Compile Include="SettingModels\SickRageSettings.cs" />
<Compile Include="SettingModels\CouchPotatoSettings.cs" />
@ -101,7 +97,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PlexRequests.Api.Interfaces\PlexRequests.Api.Interfaces.csproj">
<Project>{95834072-a675-415d-aa8f-877c91623810}</Project>
<Project>{95834072-A675-415D-AA8F-877C91623810}</Project>
<Name>PlexRequests.Api.Interfaces</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Api.Models\PlexRequests.Api.Models.csproj">

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

@ -0,0 +1,36 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SickRageSettings.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 NLog;
namespace PlexRequests.Core.SettingModels
{
public class LogSettings : Settings
{
public int Level { get; set; }
}
}

@ -44,6 +44,13 @@ namespace PlexRequests.Core.SettingModels
public int WeeklyRequestLimit { get; set; }
public string NoApprovalUsers { get; set; }
/// <summary>
/// The CSS name of the theme we want
/// </summary>
public string ThemeName { get; set; }
public string ApiKey { get; set; }
[JsonIgnore]
public List<string> ApprovalWhiteList
{

@ -0,0 +1,37 @@
using System;
using Newtonsoft.Json;
namespace PlexRequests.Core.SettingModels
{
public class SlackNotificationSettings : Settings
{
public bool Enabled { get; set; }
public string WebhookUrl { get; set; }
public string Channel { get; set; }
public string Username { get; set; }
[JsonIgnore]
public string Team => SplitWebUrl(3);
[JsonIgnore]
public string Service => SplitWebUrl(4);
[JsonIgnore]
public string Token => SplitWebUrl(5);
private string SplitWebUrl(int index)
{
if (!WebhookUrl.StartsWith("http", StringComparison.InvariantCulture))
{
WebhookUrl = "https://" + WebhookUrl;
}
var split = WebhookUrl.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
return split.Length < index
? string.Empty
: split[index];
}
}
}

@ -28,6 +28,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Mono.Data.Sqlite;
using NLog;
@ -58,10 +59,9 @@ namespace PlexRequests.Core
var version = CheckSchema();
if (version > 0)
{
if (version > 1300 && version <= 1699)
if (version > 1700 && version <= 1799)
{
MigrateDbFrom1300();
UpdateRequestBlobsTable();
MigrateToVersion1700();
}
}
@ -73,18 +73,19 @@ namespace PlexRequests.Core
private int CheckSchema()
{
var checker = new StatusChecker();
var status = checker.GetStatus();
var productVersion = AssemblyHelper.GetProductVersion();
var trimStatus = new Regex("[^0-9]", RegexOptions.Compiled).Replace(productVersion, string.Empty).PadRight(4, '0');
var version = int.Parse(trimStatus);
var connection = Db.DbConnection();
var schema = connection.GetSchemaVersion();
if (schema == null)
{
connection.CreateSchema(status.DBVersion); // Set the default.
connection.CreateSchema(version); // Set the default.
schema = connection.GetSchemaVersion();
}
var version = schema.SchemaVersion;
version = schema.SchemaVersion;
return version;
}
@ -139,7 +140,7 @@ namespace PlexRequests.Core
}
catch (Exception ex)
{
Log.Error("Failed to cache Sonarr quality profiles!", ex);
Log.Error(ex, "Failed to cache Sonarr quality profiles!");
}
}
@ -161,97 +162,14 @@ namespace PlexRequests.Core
}
catch (Exception ex)
{
Log.Error("Failed to cache CouchPotato quality profiles!", ex);
Log.Error(ex, "Failed to cache CouchPotato quality profiles!");
}
}
private void UpdateRequestBlobsTable() // TODO: Remove in v1.7
{
try
{
TableCreation.AlterTable(Db.DbConnection(), "RequestBlobs", "ADD COLUMN", "MusicId", false, "TEXT");
}
catch (Exception e)
{
Log.Error("Tried updating the schema to alter the request blobs table");
Log.Error(e);
}
}
private void MigrateDbFrom1300() // TODO: Remove in v1.7
public void MigrateToVersion1700()
{
var result = new List<long>();
RequestedModel[] requestedModels;
var repo = new GenericRepository<RequestedModel>(Db, new MemoryCacheProvider());
try
{
var records = repo.GetAll();
requestedModels = records as RequestedModel[] ?? records.ToArray();
}
catch (SqliteException)
{
// There is no requested table so they do not have an old version of the DB
return;
}
if (!requestedModels.Any())
{ return; }
var jsonRepo = new JsonRequestService(new RequestJsonRepository(Db, new MemoryCacheProvider()));
var api = new TvMazeApi();
foreach (var r in requestedModels.Where(x => x.Type == RequestType.TvShow))
{
var show = api.ShowLookupByTheTvDbId(r.ProviderId);
var model = new RequestedModel
{
Title = show.name,
PosterPath = show.image?.medium,
Type = RequestType.TvShow,
ProviderId = show.externals.thetvdb ?? 0,
ReleaseDate = r.ReleaseDate,
AdminNote = r.AdminNote,
Approved = r.Approved,
Available = r.Available,
ImdbId = show.externals.imdb,
Issues = r.Issues,
OtherMessage = r.OtherMessage,
Overview = show.summary.RemoveHtml(),
RequestedUsers = r.AllUsers, // should pull in the RequestedBy property and merge with RequestedUsers
RequestedDate = r.ReleaseDate,
Status = show.status
};
var id = jsonRepo.AddRequest(model);
result.Add(id);
}
foreach (var source in requestedModels.Where(x => x.Type == RequestType.Movie))
{
var id = jsonRepo.AddRequest(source);
result.Add(id);
}
if (result.Any(x => x == -1))
{
throw new SqliteException("Could not migrate the DB!");
}
if (result.Count != requestedModels.Length)
{
throw new SqliteException("Could not migrate the DB! count is different");
}
// Now delete the old requests
foreach (var oldRequest in requestedModels)
{
repo.Delete(oldRequest);
}
// Drop old tables
TableCreation.DropTable(Db.DbConnection(), "User");
TableCreation.DropTable(Db.DbConnection(), "Log");
}
}
}

@ -36,21 +36,20 @@ using Nancy.Security;
using PlexRequests.Core.Models;
using PlexRequests.Helpers;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
namespace PlexRequests.Core
{
public class UserMapper : IUserMapper
public class UserMapper : IUserMapper, ICustomUserMapper
{
public UserMapper(ISqliteConfiguration db)
public UserMapper(IRepository<UsersModel> repo)
{
Db = db;
Repo = repo;
}
private static ISqliteConfiguration Db { get; set; }
private static IRepository<UsersModel> Repo { get; set; }
public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context)
{
var repo = new UserRepository<UsersModel>(Db);
var user = repo.Get(identifier.ToString());
var user = Repo.Get(identifier.ToString());
if (user == null)
{
@ -64,10 +63,9 @@ namespace PlexRequests.Core
};
}
public static Guid? ValidateUser(string username, string password)
public Guid? ValidateUser(string username, string password)
{
var repo = new UserRepository<UsersModel>(Db);
var users = repo.GetAll();
var users = Repo.GetAll();
foreach (var u in users)
{
@ -83,17 +81,15 @@ namespace PlexRequests.Core
return null;
}
public static bool DoUsersExist()
public bool DoUsersExist()
{
var repo = new UserRepository<UsersModel>(Db);
var users = repo.GetAll();
var users = Repo.GetAll();
return users.Any();
}
public static Guid? CreateUser(string username, string password, string[] claims = default(string[]))
public Guid? CreateUser(string username, string password, string[] claims = default(string[]))
{
var repo = new UserRepository<UsersModel>(Db);
var salt = PasswordHasher.GenerateSalt();
var userModel = new UsersModel
@ -105,17 +101,16 @@ namespace PlexRequests.Core
Claims = ByteConverterHelper.ReturnBytes(claims),
UserProperties = ByteConverterHelper.ReturnBytes(new UserProperties())
};
repo.Insert(userModel);
Repo.Insert(userModel);
var userRecord = repo.Get(userModel.UserGuid);
var userRecord = Repo.Get(userModel.UserGuid);
return new Guid(userRecord.UserGuid);
}
public static bool UpdatePassword(string username, string oldPassword, string newPassword)
public bool UpdatePassword(string username, string oldPassword, string newPassword)
{
var repo = new UserRepository<UsersModel>(Db);
var users = repo.GetAll();
var users = Repo.GetAll();
var userToChange = users.FirstOrDefault(x => x.UserName == username);
if (userToChange == null)
return false;
@ -132,13 +127,22 @@ namespace PlexRequests.Core
userToChange.Hash = newHash;
userToChange.Salt = newSalt;
return repo.Update(userToChange);
return Repo.Update(userToChange);
}
public static IEnumerable<UsersModel> GetUsers()
public IEnumerable<UsersModel> GetUsers()
{
var repo = new UserRepository<UsersModel>(Db);
return repo.GetAll();
return Repo.GetAll();
}
}
public interface ICustomUserMapper
{
IEnumerable<UsersModel> GetUsers();
Guid? CreateUser(string username, string password, string[] claims = default(string[]));
bool DoUsersExist();
Guid? ValidateUser(string username, string password);
bool UpdatePassword(string username, string oldPassword, string newPassword);
}
}

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /></startup></configuration>

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Nancy" version="1.4.3" targetFramework="net452" />
<package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net452" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net452" />
<package id="NLog" version="4.2.3" targetFramework="net46" />
<package id="Octokit" version="0.19.0" targetFramework="net46" />
<package id="valueinjecter" version="3.1.1.2" targetFramework="net452" />
<package id="Nancy" version="1.4.3" targetFramework="net45" />
<package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net45" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net45" />
<package id="NLog" version="4.2.3" targetFramework="net45" />
<package id="Octokit" version="0.19.0" targetFramework="net45" />
<package id="valueinjecter" version="3.1.1.2" targetFramework="net45" />
</packages>

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexRegistry.cs
// File: ApplicationSettingsException.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -24,18 +24,19 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using FluentScheduler;
using PlexRequests.Services;
namespace PlexRequests.UI.Jobs
namespace PlexRequests.Helpers.Exceptions
{
public class MediaCacheRegistry : Registry
public class ApiRequestException : Exception
{
public MediaCacheRegistry()
public ApiRequestException(string message) : base(message)
{
}
public ApiRequestException(string message, Exception innerException) : base(message, innerException)
{
Schedule<MediaCacheService>().ToRunNow();
}
}
}

@ -61,7 +61,7 @@ namespace PlexRequests.Helpers
return dumpTarget.ToString();
}
public static void ConfigureLogging(string connectionString)
public static void ConfigureLogging(string connectionString)
{
LogManager.ThrowExceptions = true;
// Step 1. Create configuration object

@ -73,8 +73,8 @@ namespace PlexRequests.Helpers
/// <returns></returns>
public T Get<T>(string key) where T : class
{
var item = Cache.Get(key) as T;
return item;
lock (key)
return Cache.Get(key) as T;
}
/// <summary>
@ -86,7 +86,11 @@ namespace PlexRequests.Helpers
public void Set(string key, object data, int cacheTime = 20)
{
var policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(cacheTime) };
Cache.Add(new CacheItem(key, data), policy);
lock (key)
{
Cache.Remove(key);
Cache.Add(new CacheItem(key, data), policy);
}
}
/// <summary>
@ -98,7 +102,10 @@ namespace PlexRequests.Helpers
var keys = Cache.Where(x => x.Key.Contains(key));
foreach (var k in keys)
{
Cache.Remove(k.Key);
lock (key)
{
Cache.Remove(k.Key);
}
}
}
}

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PlexRequests.Helpers</RootNamespace>
<AssemblyName>PlexRequests.Helpers</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
@ -31,14 +31,6 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Caching" />
@ -48,11 +40,18 @@
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="AssemblyHelper.cs" />
<Compile Include="ByteConverterHelper.cs" />
<Compile Include="DateTimeHelper.cs" />
<Compile Include="Exceptions\ApiRequestException.cs" />
<Compile Include="Exceptions\ApplicationSettingsException.cs" />
<Compile Include="HtmlRemover.cs" />
<Compile Include="ICacheProvider.cs" />
@ -65,6 +64,7 @@
<Compile Include="SerializerSettings.cs" />
<Compile Include="StringCipher.cs" />
<Compile Include="UriHelper.cs" />
<Compile Include="UserClaims.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

@ -0,0 +1,12 @@
using System;
namespace PlexRequests.Helpers
{
public class UserClaims
{
public const string Admin = "Admin"; // Can do everything including creating new users and editing settings
public const string PowerUser = "PowerUser"; // Can only manage the requests, approve etc.
public const string User = "User"; // Can only request
}
}

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net452" />
<package id="NLog" version="4.2.3" targetFramework="net46" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net45" />
<package id="NLog" version="4.2.3" targetFramework="net45" />
</packages>

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /></startup></configuration>

@ -1,93 +0,0 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: AvailabilityUpdateService.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.Concurrent;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Web.Hosting;
using FluentScheduler;
using Mono.Data.Sqlite;
using NLog;
using PlexRequests.Api;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using System.Threading.Tasks;
namespace PlexRequests.Services
{
public class AvailabilityUpdateService : ITask, IRegisteredObject, IAvailabilityUpdateService
{
public AvailabilityUpdateService()
{
var memCache = new MemoryCacheProvider();
var dbConfig = new DbConfiguration(new SqliteFactory());
var repo = new SettingsJsonRepository(dbConfig, memCache);
ConfigurationReader = new ConfigurationReader();
Checker = new PlexAvailabilityChecker(new SettingsServiceV2<PlexSettings>(repo), new SettingsServiceV2<AuthenticationSettings>(repo), new JsonRequestService(new RequestJsonRepository(dbConfig, memCache)), new PlexApi(), memCache);
HostingEnvironment.RegisterObject(this);
}
private static Logger Log = LogManager.GetCurrentClassLogger();
private IConfigurationReader ConfigurationReader { get; }
private IAvailabilityChecker Checker { get; }
private IDisposable UpdateSubscription { get; set; }
public void Start(Configuration c)
{
UpdateSubscription?.Dispose();
Task.Factory.StartNew(() => Checker.CheckAndUpdateAll(-1)); // cache the libraries and run the availability checks
UpdateSubscription = Observable.Interval(c.Intervals.Notification).Subscribe(Checker.CheckAndUpdateAll);
}
public void Execute()
{
Start(ConfigurationReader.Read());
}
public void Stop(bool immediate)
{
HostingEnvironment.UnregisterObject(this);
}
}
public interface IAvailabilityUpdateService
{
void Start(Configuration c);
}
}

@ -31,7 +31,7 @@ namespace PlexRequests.Services.Interfaces
{
public interface IAvailabilityChecker
{
void CheckAndUpdateAll(long check);
void CheckAndUpdateAll();
List<PlexMovie> GetPlexMovies();
bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year);
List<PlexTvShow> GetPlexTvShows();

@ -2,7 +2,7 @@
{
public interface ICouchPotatoCacher
{
void Queued(long check);
void Queued();
int[] QueuedIds();
}
}

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: IConfigurationReader.cs
// File: IJobRecord.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -26,8 +26,8 @@
#endregion
namespace PlexRequests.Services.Interfaces
{
public interface IConfigurationReader
public interface IJobRecord
{
Configuration Read();
void Record(string jobName);
}
}

@ -36,7 +36,12 @@ namespace PlexRequests.Services.Interfaces
string NotificationName { get; }
Task NotifyAsync(NotificationModel model);
/// <summary>
/// Sends a notification to the user, this is usually for testing the settings.
/// </summary>
/// <param name="model">The model.</param>
/// <param name="settings">The settings.</param>
/// <returns></returns>
Task NotifyAsync(NotificationModel model, Settings settings);
}
}

@ -33,7 +33,18 @@ namespace PlexRequests.Services.Interfaces
{
public interface INotificationService
{
/// <summary>
/// Sends a notification to the user. This one is used in normal notification scenarios
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
Task Publish(NotificationModel model);
/// <summary>
/// Sends a notification to the user, this is usually for testing the settings.
/// </summary>
/// <param name="model">The model.</param>
/// <param name="settings">The settings.</param>
/// <returns></returns>
Task Publish(NotificationModel model, Settings settings);
void Subscribe(INotification notification);
void UnSubscribe(INotification notification);

@ -2,7 +2,7 @@
{
public interface ISickRageCacher
{
void Queued(long check);
void Queued();
int[] QueuedIds();
}
}

@ -2,7 +2,7 @@
{
public interface ISonarrCacher
{
void Queued(long check);
void Queued();
int[] QueuedIds();
}
}

@ -24,46 +24,62 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Linq;
using System;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Movie;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Api.Models.Movie;
using System.Linq;
namespace PlexRequests.Services
using Quartz;
namespace PlexRequests.Services.Jobs
{
public class CouchPotatoCacher : ICouchPotatoCacher
public class CouchPotatoCacher : IJob, ICouchPotatoCacher
{
public CouchPotatoCacher(ISettingsService<CouchPotatoSettings> cpSettings, ICouchPotatoApi cpApi, ICacheProvider cache)
public CouchPotatoCacher(ISettingsService<CouchPotatoSettings> cpSettings, ICouchPotatoApi cpApi, ICacheProvider cache, IJobRecord rec)
{
CpSettings = cpSettings;
CpApi = cpApi;
Cache = cache;
Job = rec;
}
private ISettingsService<CouchPotatoSettings> CpSettings { get; }
private ICacheProvider Cache { get; }
private ICouchPotatoApi CpApi { get; }
private IJobRecord Job { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public void Queued(long check)
public void Queued()
{
Log.Trace("This is check no. {0}", check);
Log.Trace("Getting the settings");
var settings = CpSettings.GetSettings();
if (settings.Enabled)
{
Log.Trace("Getting all movies from CouchPotato");
var movies = CpApi.GetMovies(settings.FullUri, settings.ApiKey, new[] { "active" });
Cache.Set(CacheKeys.CouchPotatoQueued, movies, 10);
try
{
var movies = CpApi.GetMovies(settings.FullUri, settings.ApiKey, new[] { "active" });
if (movies != null)
{
Cache.Set(CacheKeys.CouchPotatoQueued, movies, CacheKeys.TimeFrameMinutes.SchedulerCaching);
}
}
catch (System.Exception ex)
{
Log.Error(ex, "Failed caching queued items from CouchPotato");
}
finally
{
Job.Record(JobNames.CpCacher);
}
}
}
@ -71,7 +87,12 @@ namespace PlexRequests.Services
public int[] QueuedIds()
{
var movies = Cache.Get<CouchPotatoMovies>(CacheKeys.CouchPotatoQueued);
return movies != null ? movies.movies.Select(x => x.info.tmdb_id).ToArray() : new int[] { };
return movies?.movies.Select(x => x.info.tmdb_id).ToArray() ?? new int[] { };
}
public void Execute(IJobExecutionContext context)
{
Queued();
}
}
}

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: Congifuration.cs
// File: JobNames.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -24,17 +24,14 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using PlexRequests.Services.Interfaces;
namespace PlexRequests.Services
{
public class Configuration
public static class JobNames
{
public Configuration(IIntervals intervals)
{
Intervals = intervals;
}
public IIntervals Intervals { get; set; }
public const string StoreBackup = "Database Backup";
public const string CpCacher = "CouchPotato Cacher";
public const string SonarrCacher = "Sonarr Cacher";
public const string SrCacher = "SickRage Cacher";
public const string PlexChecker = "Plex Availability Cacher";
}
}

@ -0,0 +1,59 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: JobRecord.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 PlexRequests.Services.Interfaces;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
namespace PlexRequests.Services
{
public class JobRecord : IJobRecord
{
public JobRecord(IRepository<ScheduledJobs> repo)
{
Repo = repo;
}
private IRepository<ScheduledJobs> Repo { get; }
public void Record(string jobName)
{
var allJobs = Repo.GetAll();
var storeJob = allJobs.FirstOrDefault(x => x.Name == jobName);
if (storeJob != null)
{
storeJob.LastRun = DateTime.UtcNow;
Repo.Update(storeJob);
}
else
{
var job = new ScheduledJobs { LastRun = DateTime.UtcNow, Name = jobName };
Repo.Insert(job);
}
}
}
}

@ -36,20 +36,29 @@ using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
using PlexRequests.Services.Models;
using PlexRequests.Services.Notification;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using Quartz;
namespace PlexRequests.Services
namespace PlexRequests.Services.Jobs
{
public class PlexAvailabilityChecker : IAvailabilityChecker
public class PlexAvailabilityChecker : IJob, IAvailabilityChecker
{
public PlexAvailabilityChecker(ISettingsService<PlexSettings> plexSettings, ISettingsService<AuthenticationSettings> auth, IRequestService request, IPlexApi plex, ICacheProvider cache)
public PlexAvailabilityChecker(ISettingsService<PlexSettings> plexSettings, ISettingsService<AuthenticationSettings> auth, IRequestService request, IPlexApi plex, ICacheProvider cache,
INotificationService notify, IJobRecord rec, IRepository<UsersToNotify> users)
{
Plex = plexSettings;
Auth = auth;
RequestService = request;
PlexApi = plex;
Cache = cache;
Notification = notify;
Job = rec;
UserNotifyRepo = users;
}
private ISettingsService<PlexSettings> Plex { get; }
@ -58,10 +67,11 @@ namespace PlexRequests.Services
private static Logger Log = LogManager.GetCurrentClassLogger();
private IPlexApi PlexApi { get; }
private ICacheProvider Cache { get; }
public void CheckAndUpdateAll(long check)
private INotificationService Notification { get; }
private IJobRecord Job { get; }
private IRepository<UsersToNotify> UserNotifyRepo { get; }
public void CheckAndUpdateAll()
{
Log.Trace("This is check no. {0}", check);
Log.Trace("Getting the settings");
var plexSettings = Plex.GetSettings();
var authSettings = Auth.GetSettings();
@ -74,6 +84,13 @@ namespace PlexRequests.Services
}
var libraries = CachedLibraries(authSettings, plexSettings, true); //force setting the cache (10 min intervals via scheduler)
if (libraries == null || !libraries.Any())
{
Log.Info("Did not find any libraries in Plex.");
return;
}
var movies = GetPlexMovies().ToArray();
var shows = GetPlexTvShows().ToArray();
var albums = GetPlexAlbums().ToArray();
@ -93,18 +110,7 @@ namespace PlexRequests.Services
{
Log.Trace("We are going to see if Plex has the following title: {0}", r.Title);
if (libraries == null)
{
libraries = new List<PlexSearch>() { PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri) };
if (libraries == null)
{
Log.Trace("Could not find any matching result for this title.");
continue;
}
}
Log.Trace("Search results from Plex for the following request: {0}", r.Title);
//Log.Trace(results.DumpJson());
var releaseDate = r.ReleaseDate == DateTime.MinValue ? string.Empty : r.ReleaseDate.ToString("yyyy");
@ -135,13 +141,16 @@ namespace PlexRequests.Services
}
Log.Trace("Updating the requests now");
Log.Trace("Requests that will be updates:");
Log.Trace(modifiedModel.SelectMany(x => x.Title).DumpJson());
Log.Trace("Requests that will be updated count {0}", modifiedModel.Count);
if (modifiedModel.Any())
{
NotifyUsers(modifiedModel, authSettings.PlexAuthToken);
RequestService.BatchUpdate(modifiedModel);
}
Job.Record(JobNames.PlexChecker);
}
public List<PlexMovie> GetPlexMovies()
@ -239,7 +248,7 @@ namespace PlexRequests.Services
private List<PlexSearch> CachedLibraries(AuthenticationSettings authSettings, PlexSettings plexSettings, bool setCache)
{
Log.Trace("Obtaining library sections from Plex for the following request");
Log.Trace("Obtaining library sections from Plex");
List<PlexSearch> results = new List<PlexSearch>();
@ -249,17 +258,34 @@ namespace PlexRequests.Services
return results; // don't error out here, just let it go!
}
if (setCache)
try
{
results = GetLibraries(authSettings, plexSettings);
Cache.Set(CacheKeys.PlexLibaries, results, 10);
if (setCache)
{
Log.Trace("Plex Lib API Call");
results = GetLibraries(authSettings, plexSettings);
Log.Trace("Plex Lib Cache Set Call");
if (results != null)
{
Cache.Set(CacheKeys.PlexLibaries, results, CacheKeys.TimeFrameMinutes.SchedulerCaching);
}
}
else
{
Log.Trace("Plex Lib GetSet Call");
results = Cache.GetOrSet(CacheKeys.PlexLibaries, () =>
{
Log.Trace("Plex Lib API Call (inside getset)");
return GetLibraries(authSettings, plexSettings);
}, CacheKeys.TimeFrameMinutes.SchedulerCaching);
}
}
else
catch (Exception ex)
{
results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => {
return GetLibraries(authSettings, plexSettings);
}, 10);
Log.Error(ex, "Failed to obtain Plex libraries");
}
return results;
}
@ -281,6 +307,7 @@ namespace PlexRequests.Services
}
}
Log.Trace("Returning Plex Libs");
return libs;
}
@ -293,5 +320,51 @@ namespace PlexRequests.Services
}
return true;
}
private void NotifyUsers(IEnumerable<RequestedModel> modelChanged, string apiKey)
{
try
{
var plexUser = PlexApi.GetUsers(apiKey);
if (plexUser?.User == null || plexUser.User.Length == 0)
{
return;
}
var users = UserNotifyRepo.GetAll().ToList();
foreach (var model in modelChanged)
{
var selectedUsers = users.Select(x => x.Username).Intersect(model.RequestedUsers);
foreach (var user in selectedUsers)
{
var email = plexUser.User.FirstOrDefault(x => x.Username == user);
if (email == null)
{
// We do not have a plex user that requested this!
continue;
}
var notificationModel = new NotificationModel
{
User = email.Username,
UserEmail = email.Email,
NotificationType = NotificationType.RequestAvailable,
Title = model.Title
};
// Send the notification to the user.
Notification.Publish(notificationModel);
}
}
}
catch (Exception e)
{
Log.Error(e);
}
}
public void Execute(IJobExecutionContext context)
{
CheckAndUpdateAll();
}
}
}

@ -24,28 +24,29 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Linq;
using System;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.SickRage;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Api.Models.Movie;
using System.Linq;
using PlexRequests.Api.Models.SickRage;
namespace PlexRequests.Services
using Quartz;
namespace PlexRequests.Services.Jobs
{
public class SickRageCacher : ISickRageCacher
public class SickRageCacher : IJob, ISickRageCacher
{
public SickRageCacher(ISettingsService<SickRageSettings> srSettings, ISickRageApi srApi, ICacheProvider cache)
public SickRageCacher(ISettingsService<SickRageSettings> srSettings, ISickRageApi srApi, ICacheProvider cache, IJobRecord rec)
{
SrSettings = srSettings;
SrApi = srApi;
Cache = cache;
Job = rec;
}
private ISettingsService<SickRageSettings> SrSettings { get; }
@ -53,18 +54,32 @@ namespace PlexRequests.Services
private ISickRageApi SrApi { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
private IJobRecord Job { get; }
public void Queued(long check)
public void Queued()
{
Log.Trace("This is check no. {0}", check);
Log.Trace("Getting the settings");
var settings = SrSettings.GetSettings();
if (settings.Enabled)
{
Log.Trace("Getting all shows from SickRage");
var movies = SrApi.GetShows(settings.ApiKey, settings.FullUri);
Cache.Set(CacheKeys.SickRageQueued, movies.Result);
try
{
var shows = SrApi.GetShows(settings.ApiKey, settings.FullUri);
if (shows != null)
{
Cache.Set(CacheKeys.SickRageQueued, shows.Result, CacheKeys.TimeFrameMinutes.SchedulerCaching);
}
}
catch (System.Exception ex)
{
Log.Error(ex, "Failed caching queued items from SickRage");
}
finally
{
Job.Record(JobNames.SrCacher);
}
}
}
@ -74,5 +89,10 @@ namespace PlexRequests.Services
var tv = Cache.Get<SickrageShows>(CacheKeys.SickRageQueued);
return tv?.data.Values.Select(x => x.tvdbid).ToArray() ?? new int[] { };
}
public void Execute(IJobExecutionContext context)
{
Queued();
}
}
}

@ -24,46 +24,65 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Linq;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using System.Linq;
using System.Collections.Generic;
using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using Quartz;
namespace PlexRequests.Services
namespace PlexRequests.Services.Jobs
{
public class SonarrCacher : ISonarrCacher
public class SonarrCacher : IJob, ISonarrCacher
{
public SonarrCacher(ISettingsService<SonarrSettings> sonarrSettings, ISonarrApi sonarrApi, ICacheProvider cache)
public SonarrCacher(ISettingsService<SonarrSettings> sonarrSettings, ISonarrApi sonarrApi, ICacheProvider cache, IJobRecord rec)
{
SonarrSettings = sonarrSettings;
SonarrApi = sonarrApi;
Job = rec;
Cache = cache;
}
private ISettingsService<SonarrSettings> SonarrSettings { get; }
private ICacheProvider Cache { get; }
private ISonarrApi SonarrApi { get; }
private IJobRecord Job { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public void Queued(long check)
public void Queued()
{
Log.Trace("This is check no. {0}", check);
Log.Trace("Getting the settings");
var settings = SonarrSettings.GetSettings();
if (settings.Enabled)
{
Log.Trace("Getting all tv series from Sonarr");
var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri);
Cache.Set(CacheKeys.SonarrQueued, series, 10);
try
{
var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri);
if (series != null)
{
Cache.Set(CacheKeys.SonarrQueued, series, CacheKeys.TimeFrameMinutes.SchedulerCaching);
}
}
catch (System.Exception ex)
{
Log.Error(ex, "Failed caching queued items from Sonarr");
}
finally
{
Job.Record(JobNames.SonarrCacher);
}
}
}
@ -73,5 +92,10 @@ namespace PlexRequests.Services
var series = Cache.Get<List<Series>>(CacheKeys.SonarrQueued);
return series?.Select(x => x.tvdbId).ToArray() ?? new int[] { };
}
public void Execute(IJobExecutionContext context)
{
Queued();
}
}
}

@ -0,0 +1,150 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: StoreBackup.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.IO;
using System.Linq;
using System.Globalization;
using NLog;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using Quartz;
namespace PlexRequests.Services.Jobs
{
public class StoreBackup : IJob
{
public StoreBackup(ISqliteConfiguration sql, IJobRecord rec)
{
Sql = sql;
JobRecord = rec;
}
private ISqliteConfiguration Sql { get; }
private IJobRecord JobRecord { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public void Execute(IJobExecutionContext context)
{
TakeBackup();
Cleanup ();
}
private void TakeBackup()
{
Log.Trace("Starting DB Backup");
var dbPath = Sql.CurrentPath;
var dir = Path.GetDirectoryName(dbPath);
if (dir == null)
{
Log.Warn("We couldn't find the DB path. We cannot backup.");
return;
}
var backupDir = Directory.CreateDirectory(Path.Combine(dir, "Backup"));
if (string.IsNullOrEmpty(dbPath))
{
Log.Warn("Could not find the actual database. We cannot backup.");
return;
}
try
{
if(DoWeNeedToBackup(backupDir.FullName))
{
File.Copy(dbPath, Path.Combine(backupDir.FullName, $"PlexRequests.sqlite_{DateTime.Now.ToString("yyyy-MM-dd hh.mm.ss")}.bak"));
}
}
catch (Exception e)
{
Log.Warn(e);
Log.Warn("Exception when trying to copy the backup.");
}
finally
{
JobRecord.Record(JobNames.StoreBackup);
}
}
private void Cleanup()
{
Log.Trace("Starting DB Cleanup");
var dbPath = Sql.CurrentPath;
var dir = Path.GetDirectoryName(dbPath);
if (dir == null)
{
Log.Warn("We couldn't find the DB path. We cannot backup.");
return;
}
var backupDir = Directory.CreateDirectory(Path.Combine(dir, "Backup"));
var files = backupDir.GetFiles();
foreach (var file in files) {
var dt = ParseName(file.Name);
if(dt < DateTime.Now.AddDays(-7)){
try {
File.Delete(file.FullName);
} catch (Exception ex) {
Log.Error(ex);
}
}
}
}
private bool DoWeNeedToBackup(string backupPath)
{
var files = Directory.GetFiles(backupPath);
//TODO Get the latest file and if it's within an hour of DateTime.Now then don't bother backing up.
return true;
}
private DateTime ParseName(string fileName)
{
var names = fileName.Split(new []{'_','.',' '}, StringSplitOptions.RemoveEmptyEntries);
if(names.Count() > 1)
{
DateTime parsed;
//DateTime.TryParseExcat(names[1], "yyyy-MM-dd hh.mm.ss",CultureInfo.CurrentUICulture, DateTimeStyles.None, out parsed);
DateTime.TryParse(names[2], out parsed);
return parsed;
}
return DateTime.MinValue;
}
}
}

@ -1,102 +0,0 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: AvailabilityUpdateService.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.Reactive.Linq;
using System.Web.Hosting;
using FluentScheduler;
using Mono.Data.Sqlite;
using NLog;
using PlexRequests.Api;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using System.Threading.Tasks;
namespace PlexRequests.Services
{
public class MediaCacheService : ITask, IRegisteredObject, IAvailabilityUpdateService
{
public MediaCacheService()
{
var memCache = new MemoryCacheProvider();
var dbConfig = new DbConfiguration(new SqliteFactory());
var repo = new SettingsJsonRepository(dbConfig, memCache);
ConfigurationReader = new ConfigurationReader();
CpCacher = new CouchPotatoCacher(new SettingsServiceV2<CouchPotatoSettings>(repo), new CouchPotatoApi(), memCache);
SonarrCacher = new SonarrCacher(new SettingsServiceV2<SonarrSettings>(repo), new SonarrApi(), memCache);
SickRageCacher = new SickRageCacher(new SettingsServiceV2<SickRageSettings>(repo), new SickrageApi(), memCache);
HostingEnvironment.RegisterObject(this);
}
private static Logger Log = LogManager.GetCurrentClassLogger();
private IConfigurationReader ConfigurationReader { get; }
private ICouchPotatoCacher CpCacher { get; }
private ISonarrCacher SonarrCacher { get; }
private ISickRageCacher SickRageCacher { get; }
private IDisposable CpSubscription { get; set; }
private IDisposable SonarrSubscription { get; set; }
private IDisposable SickRageSubscription { get; set; }
public void Start(Configuration c)
{
CpSubscription?.Dispose();
SonarrSubscription?.Dispose();
SickRageSubscription?.Dispose();
Task.Factory.StartNew(() => CpCacher.Queued(-1));
Task.Factory.StartNew(() => SonarrCacher.Queued(-1));
Task.Factory.StartNew(() => SickRageCacher.Queued(-1));
CpSubscription = Observable.Interval(c.Intervals.Notification).Subscribe(CpCacher.Queued);
SonarrSubscription = Observable.Interval(c.Intervals.Notification).Subscribe(SonarrCacher.Queued);
SickRageSubscription = Observable.Interval(c.Intervals.Notification).Subscribe(SickRageCacher.Queued);
}
public void Execute()
{
Start(ConfigurationReader.Read());
}
public void Stop(bool immediate)
{
HostingEnvironment.UnregisterObject(this);
}
}
public interface ICouchPotatoCacheService
{
void Start(Configuration c);
}
}

@ -25,15 +25,17 @@
// ************************************************************************/
#endregion
using System;
using System.Net;
using System.Net.Mail;
using System.Threading.Tasks;
using MailKit.Security;
using MimeKit;
using NLog;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Services.Interfaces;
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
namespace PlexRequests.Services.Notification
{
@ -71,8 +73,8 @@ namespace PlexRequests.Services.Notification
await EmailIssue(model, emailSettings);
break;
case NotificationType.RequestAvailable:
throw new NotImplementedException();
await EmailAvailableRequest(model, emailSettings);
break;
case NotificationType.RequestApproved:
throw new NotImplementedException();
@ -111,58 +113,68 @@ namespace PlexRequests.Services.Notification
private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings)
{
var message = new MailMessage
var message = new MimeMessage
{
IsBodyHtml = true,
To = { new MailAddress(settings.RecipientEmail) },
Body = $"Hello! The user '{model.User}' has requested {model.Title}! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}",
From = new MailAddress(settings.EmailSender),
Body = new TextPart("plain") { Text = $"Hello! The user '{model.User}' has requested {model.Title}! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}" },
Subject = $"Plex Requests: New request for {model.Title}!"
};
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail));
try
{
using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort))
{
smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
smtp.EnableSsl = settings.Ssl;
await smtp.SendMailAsync(message).ConfigureAwait(false);
}
}
catch (SmtpException smtp)
{
Log.Error(smtp);
}
catch (Exception e)
{
Log.Error(e);
}
await Send(message, settings);
}
private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings)
{
var message = new MailMessage
var message = new MimeMessage
{
IsBodyHtml = true,
To = { new MailAddress(settings.RecipientEmail) },
Body = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!",
From = new MailAddress(settings.RecipientEmail),
Body = new TextPart("plain") { Text = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!" },
Subject = $"Plex Requests: New issue for {model.Title}!"
};
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail));
await Send(message, settings);
}
private async Task EmailAvailableRequest(NotificationModel model, EmailNotificationSettings settings)
{
if (!settings.EnableUserEmailNotifications)
{
await Task.FromResult(false);
}
var message = new MimeMessage
{
Body = new TextPart("plain") { Text = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)" },
Subject = $"Plex Requests: {model.Title} is now available!"
};
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
message.To.Add(new MailboxAddress(model.UserEmail, model.UserEmail));
await Send(message, settings);
}
private async Task Send(MimeMessage message, EmailNotificationSettings settings)
{
try
{
using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort))
using (var client = new SmtpClient())
{
smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
smtp.EnableSsl = settings.Ssl;
await smtp.SendMailAsync(message).ConfigureAwait(false);
client.Connect(settings.EmailHost, settings.EmailPort, SecureSocketOptions.Auto); // Let MailKit figure out the correct SecureSocketOptions.
// Note: since we don't have an OAuth2 token, disable
// the XOAUTH2 authentication mechanism.
client.AuthenticationMechanisms.Remove("XOAUTH2");
client.Authenticate(settings.EmailUsername, settings.EmailPassword);
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
}
catch (SmtpException smtp)
{
Log.Error(smtp);
}
catch (Exception e)
{
Log.Error(e);
@ -171,32 +183,15 @@ namespace PlexRequests.Services.Notification
private async Task EmailTest(NotificationModel model, EmailNotificationSettings settings)
{
var message = new MailMessage
var message = new MimeMessage
{
IsBodyHtml = true,
To = { new MailAddress(settings.RecipientEmail) },
Body = "This is just a test! Success!",
From = new MailAddress(settings.RecipientEmail),
Subject = "Plex Requests: Test Message!"
Body = new TextPart("plain") {Text= "This is just a test! Success!"},
Subject = "Plex Requests: Test Message!",
};
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail));
try
{
using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort))
{
smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
smtp.EnableSsl = settings.Ssl;
await smtp.SendMailAsync(message).ConfigureAwait(false);
}
}
catch (SmtpException smtp)
{
Log.Error(smtp);
}
catch (Exception e)
{
Log.Error(e);
}
await Send(message, settings);
}
}
}

@ -35,5 +35,6 @@ namespace PlexRequests.Services.Notification
public DateTime DateTime { get; set; }
public NotificationType NotificationType { get; set; }
public string User { get; set; }
public string UserEmail { get; set; }
}
}

@ -41,6 +41,11 @@ namespace PlexRequests.Services.Notification
private static Logger Log = LogManager.GetCurrentClassLogger();
public ConcurrentDictionary<string, INotification> Observers { get; } = new ConcurrentDictionary<string, INotification>();
/// <summary>
/// Sends a notification to the user. This one is used in normal notification scenarios
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
public async Task Publish(NotificationModel model)
{
var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model));
@ -48,6 +53,12 @@ namespace PlexRequests.Services.Notification
await Task.WhenAll(notificationTasks).ConfigureAwait(false);
}
/// <summary>
/// Sends a notification to the user, this is usually for testing the settings.
/// </summary>
/// <param name="model">The model.</param>
/// <param name="settings">The settings.</param>
/// <returns></returns>
public async Task Publish(NotificationModel model, Settings settings)
{
var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model, settings));

@ -33,6 +33,7 @@ namespace PlexRequests.Services.Notification
RequestAvailable,
RequestApproved,
AdminNote,
Test
Test,
}
}

@ -45,7 +45,6 @@ namespace PlexRequests.Services.Notification
}
private IPushbulletApi PushbulletApi { get; }
private ISettingsService<PushbulletNotificationSettings> SettingsService { get; }
private PushbulletNotificationSettings Settings => GetSettings();
private static Logger Log = LogManager.GetCurrentClassLogger();
public string NotificationName => "PushbulletNotification";
@ -78,7 +77,7 @@ namespace PlexRequests.Services.Notification
case NotificationType.AdminNote:
break;
case NotificationType.Test:
await PushTestAsync(model, pushSettings);
await PushTestAsync(pushSettings);
break;
default:
throw new ArgumentOutOfRangeException();
@ -107,45 +106,28 @@ namespace PlexRequests.Services.Notification
{
var message = $"{model.Title} has been requested by user: {model.User}";
var pushTitle = $"Plex Requests: {model.Title} has been requested!";
try
{
var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier);
if (result == null)
{
Log.Error("Pushbullet api returned a null value, the notification did not get pushed");
}
}
catch (Exception e)
{
Log.Error(e);
}
await Push(settings, message, pushTitle);
}
private async Task PushIssueAsync(NotificationModel model, PushbulletNotificationSettings settings)
{
var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}";
var pushTitle = $"Plex Requests: A new issue has been reported for {model.Title}";
try
{
var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier);
if (result != null)
{
Log.Error("Pushbullet api returned a null value, the notification did not get pushed");
}
}
catch (Exception e)
{
Log.Error(e);
}
await Push(settings, message, pushTitle);
}
private async Task PushTestAsync(NotificationModel model, PushbulletNotificationSettings settings)
private async Task PushTestAsync(PushbulletNotificationSettings settings)
{
var message = "This is just a test! Success!";
var pushTitle = "Plex Requests: Test Message!";
await Push(settings, message, pushTitle);
}
private async Task Push(PushbulletNotificationSettings settings, string message, string title)
{
try
{
var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier);
var result = await PushbulletApi.PushAsync(settings.AccessToken, title, message, settings.DeviceIdentifier);
if (result != null)
{
Log.Error("Pushbullet api returned a null value, the notification did not get pushed");

@ -45,7 +45,6 @@ namespace PlexRequests.Services.Notification
}
private IPushoverApi PushoverApi { get; }
private ISettingsService<PushoverNotificationSettings> SettingsService { get; }
private PushoverNotificationSettings Settings => GetSettings();
private static Logger Log = LogManager.GetCurrentClassLogger();
public string NotificationName => "PushoverNotification";
@ -106,40 +105,23 @@ namespace PlexRequests.Services.Notification
private async Task PushNewRequestAsync(NotificationModel model, PushoverNotificationSettings settings)
{
var message = $"Plex Requests: {model.Title} has been requested by user: {model.User}";
try
{
var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken);
if (result?.status != 1)
{
Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed");
}
}
catch (Exception e)
{
Log.Error(e);
}
await Push(settings, message);
}
private async Task PushIssueAsync(NotificationModel model, PushoverNotificationSettings settings)
{
var message = $"Plex Requests: A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}";
try
{
var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken);
if (result?.status != 1)
{
Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed");
}
}
catch (Exception e)
{
Log.Error(e);
}
await Push(settings, message);
}
private async Task PushTestAsync(NotificationModel model, PushoverNotificationSettings settings)
{
var message = $"Plex Requests: Test Message!";
await Push(settings, message);
}
private async Task Push(PushoverNotificationSettings settings, string message)
{
try
{
var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken);

@ -0,0 +1,155 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SlackNotification.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.Threading.Tasks;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Notifications;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Services.Interfaces;
namespace PlexRequests.Services.Notification
{
public class SlackNotification : INotification
{
public SlackNotification(ISlackApi api, ISettingsService<SlackNotificationSettings> sn)
{
Api = api;
Settings = sn;
}
public string NotificationName => "SlackNotification";
private ISlackApi Api { get; }
private ISettingsService<SlackNotificationSettings> Settings { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public async Task NotifyAsync(NotificationModel model)
{
var settings = Settings.GetSettings();
await NotifyAsync(model, settings);
}
public async Task NotifyAsync(NotificationModel model, Settings settings)
{
if (settings == null) await NotifyAsync(model);
var pushSettings = (SlackNotificationSettings)settings;
if (!ValidateConfiguration(pushSettings))
{
Log.Error("Settings for Slack was not correct, we cannot push a notification");
return;
}
switch (model.NotificationType)
{
case NotificationType.NewRequest:
await PushNewRequestAsync(model, pushSettings);
break;
case NotificationType.Issue:
await PushIssueAsync(model, pushSettings);
break;
case NotificationType.RequestAvailable:
break;
case NotificationType.RequestApproved:
break;
case NotificationType.AdminNote:
break;
case NotificationType.Test:
await PushTest(pushSettings);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private async Task PushNewRequestAsync(NotificationModel model, SlackNotificationSettings settings)
{
var message = $"{model.Title} has been requested by user: {model.User}";
await Push(settings, message);
}
private async Task PushIssueAsync(NotificationModel model, SlackNotificationSettings settings)
{
var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}";
await Push(settings, message);
}
private async Task PushTest(SlackNotificationSettings settings)
{
var message = $"This is a test from Plex Requests, if you can see this then we have successfully pushed a notification!";
await Push(settings, message);
}
private async Task Push(SlackNotificationSettings config, string message)
{
try
{
var notification = new SlackNotificationBody { username = config.Username, channel = config.Channel ?? string.Empty, text = message };
var result = await Api.PushAsync(config.Team, config.Token, config.Service, notification);
if (!result.Equals("ok"))
{
Log.Error("Slack returned a message that was not 'ok', the notification did not get pushed");
Log.Error($"Message that slack returned: {result}");
}
}
catch (Exception e)
{
Log.Error(e);
}
}
private bool ValidateConfiguration(SlackNotificationSettings settings)
{
if (!settings.Enabled)
{
return false;
}
if (string.IsNullOrEmpty(settings.WebhookUrl))
{
return false;
}
try
{
var a = settings.Team;
var b = settings.Service;
var c = settings.Token;
}
catch (IndexOutOfRangeException)
{
return false;
}
return true;
}
}
}

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PlexRequests.Services</RootNamespace>
<AssemblyName>PlexRequests.Services</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
@ -31,73 +31,59 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="BouncyCastle, Version=1.8.1.0, Culture=neutral, PublicKeyToken=0e99375e54769942, processorArchitecture=MSIL">
<Reference Include="Microsoft.Build.Framework" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Security" />
<Reference Include="System.Web" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="BouncyCastle, Version=1.8.1.0, Culture=neutral, PublicKeyToken=0e99375e54769942">
<HintPath>..\packages\MimeKit.1.2.22\lib\net45\BouncyCastle.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FluentScheduler, Version=3.1.46.0, Culture=neutral, PublicKeyToken=b76503528a14ebd1, processorArchitecture=MSIL">
<HintPath>..\packages\FluentScheduler.3.1.46\lib\net40\FluentScheduler.dll</HintPath>
<Private>True</Private>
<Reference Include="Common.Logging, Version=3.0.0.0, Culture=neutral, PublicKeyToken=af08829b84f0328e">
<HintPath>..\packages\Common.Logging.3.0.0\lib\net40\Common.Logging.dll</HintPath>
</Reference>
<Reference Include="Common.Logging.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=af08829b84f0328e">
<HintPath>..\packages\Common.Logging.Core.3.0.0\lib\net40\Common.Logging.Core.dll</HintPath>
</Reference>
<Reference Include="MailKit, Version=1.2.0.0, Culture=neutral, PublicKeyToken=4e064fe7c44a8f1b, processorArchitecture=MSIL">
<Reference Include="MailKit, Version=1.2.0.0, Culture=neutral, PublicKeyToken=4e064fe7c44a8f1b">
<HintPath>..\packages\MailKit.1.2.21\lib\net451\MailKit.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Build.Framework" />
<Reference Include="MimeKit, Version=1.2.0.0, Culture=neutral, PublicKeyToken=bede1c8a46c66814, processorArchitecture=MSIL">
<Reference Include="MimeKit, Version=1.2.0.0, Culture=neutral, PublicKeyToken=bede1c8a46c66814">
<HintPath>..\packages\MimeKit.1.2.22\lib\net45\MimeKit.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Data.Sqlite, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<Reference Include="Mono.Data.Sqlite">
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
<Private>True</Private>
<Reference Include="Quartz, Version=2.3.3.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4">
<HintPath>..\packages\Quartz.2.3.3\lib\net40\Quartz.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Security" />
<Reference Include="System.Web" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Interfaces\IJobRecord.cs" />
<Compile Include="Jobs\JobRecord.cs" />
<Compile Include="Jobs\JobNames.cs" />
<Compile Include="Jobs\StoreBackup.cs" />
<Compile Include="Jobs\CouchPotatoCacher.cs" />
<Compile Include="Jobs\PlexAvailabilityChecker.cs" />
<Compile Include="Jobs\SickRageCacher.cs" />
<Compile Include="Jobs\SonarrCacher.cs" />
<Compile Include="Models\PlexAlbum.cs" />
<Compile Include="Models\PlexMovie.cs" />
<Compile Include="Models\PlexTvShow.cs" />
<Compile Include="SickRageCacher.cs" />
<Compile Include="Interfaces\ITvCacher.cs" />
<Compile Include="SonarrCacher.cs" />
<Compile Include="Interfaces\ISickRageCacher.cs" />
<Compile Include="Interfaces\ISonarrCacher.cs" />
<Compile Include="MediaCacheService.cs" />
<Compile Include="AvailabilityUpdateService.cs" />
<Compile Include="Configuration.cs" />
<Compile Include="ConfigurationReader.cs" />
<Compile Include="Interfaces\ICouchPotatoCacher.cs" />
<Compile Include="Interfaces\IAvailabilityChecker.cs" />
<Compile Include="Interfaces\IConfigurationReader.cs" />
<Compile Include="Interfaces\IIntervals.cs" />
<Compile Include="Interfaces\INotification.cs" />
<Compile Include="Interfaces\INotificationService.cs" />
@ -107,11 +93,8 @@
<Compile Include="Notification\NotificationType.cs" />
<Compile Include="Notification\PushoverNotification.cs" />
<Compile Include="Notification\PushbulletNotification.cs" />
<Compile Include="CouchPotatoCacher.cs" />
<Compile Include="PlexAvailabilityChecker.cs" />
<Compile Include="PlexType.cs" />
<Compile Include="Notification\SlackNotification.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UpdateInterval.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
@ -119,7 +102,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PlexRequests.Api.Interfaces\PlexRequests.Api.Interfaces.csproj">
<Project>{95834072-a675-415d-aa8f-877c91623810}</Project>
<Project>{95834072-A675-415D-AA8F-877C91623810}</Project>
<Name>PlexRequests.Api.Interfaces</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Api.Models\PlexRequests.Api.Models.csproj">
@ -127,11 +110,11 @@
<Name>PlexRequests.Api.Models</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Api\PlexRequests.Api.csproj">
<Project>{8cb8d235-2674-442d-9c6a-35fcaeeb160d}</Project>
<Project>{8CB8D235-2674-442D-9C6A-35FCAEEB160D}</Project>
<Name>PlexRequests.Api</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Core\PlexRequests.Core.csproj">
<Project>{dd7dc444-d3bf-4027-8ab9-efc71f5ec581}</Project>
<Project>{DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}</Project>
<Name>PlexRequests.Core</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Helpers\PlexRequests.Helpers.csproj">
@ -139,7 +122,7 @@
<Name>PlexRequests.Helpers</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Store\PlexRequests.Store.csproj">
<Project>{92433867-2b7b-477b-a566-96c382427525}</Project>
<Project>{92433867-2B7B-477B-A566-96C382427525}</Project>
<Name>PlexRequests.Store</Name>
</ProjectReference>
</ItemGroup>

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /></startup></configuration>

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="FluentScheduler" version="3.1.46" targetFramework="net452" />
<package id="MailKit" version="1.2.21" targetFramework="net46" />
<package id="MimeKit" version="1.2.22" targetFramework="net46" />
<package id="NLog" version="4.2.3" targetFramework="net452" />
<package id="Rx-Core" version="2.2.5" targetFramework="net452" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net452" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net452" />
<package id="Rx-Main" version="2.2.5" targetFramework="net452" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net452" />
<package id="Common.Logging" version="3.0.0" targetFramework="net45" />
<package id="Common.Logging.Core" version="3.0.0" targetFramework="net45" />
<package id="MailKit" version="1.2.21" targetFramework="net45" requireReinstallation="True" />
<package id="MimeKit" version="1.2.22" targetFramework="net45" />
<package id="NLog" version="4.2.3" targetFramework="net45" />
<package id="Quartz" version="2.3.3" targetFramework="net45" />
</packages>

@ -23,6 +23,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.
// ***********************************************************************
using System.Globalization;
#endregion
using System;
using System.Data;
@ -44,7 +47,7 @@ namespace PlexRequests.Store
}
private SqliteFactory Factory { get; }
private string CurrentPath =>Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, DbFile);
public string CurrentPath =>Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, DbFile);
public virtual bool CheckDb()
{
@ -59,7 +62,7 @@ namespace PlexRequests.Store
return false;
}
public string DbFile = "PlexRequests.sqlite";
public const string DbFile = "PlexRequests.sqlite";
/// <summary>
/// Gets the database connection.

@ -45,6 +45,9 @@ namespace PlexRequests.Store
/// Creates the database.
/// </summary>
void CreateDatabase();
}
string CurrentPath { get; }
}
}

@ -0,0 +1,16 @@
using System;
using Dapper.Contrib.Extensions;
namespace PlexRequests.Store
{
[Table("Audit")]
public class Audit : Entity
{
public string Username{get;set;}
public DateTime Date {get;set;}
public string ChangeType {get;set;}
public string OldValue {get;set;}
public string NewValue{get;set;}
}
}

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UpdateInterval.cs
// File: LogEntity.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -26,13 +26,14 @@
#endregion
using System;
using PlexRequests.Services.Interfaces;
using Dapper.Contrib.Extensions;
namespace PlexRequests.Services
namespace PlexRequests.Store.Models
{
public class UpdateInterval : IIntervals
[Table("ScheduledJobs")]
public class ScheduledJobs : Entity
{
public TimeSpan Notification => TimeSpan.FromMinutes(10);
public string Name { get; set; }
public DateTime LastRun { get; set; }
}
}

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

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PlexRequests.Store</RootNamespace>
<AssemblyName>PlexRequests.Store</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
@ -31,25 +31,9 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dapper, Version=1.40.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Dapper.1.42\lib\net45\Dapper.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Dapper.Contrib, Version=1.40.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Dapper.Contrib.1.43\lib\net45\Dapper.Contrib.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Data.Sqlite">
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Windows.Forms" />
@ -59,10 +43,24 @@
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="Dapper, Version=1.40.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\Dapper.1.42\lib\net45\Dapper.dll</HintPath>
</Reference>
<Reference Include="Dapper.Contrib, Version=1.40.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\Dapper.Contrib.1.43\lib\net45\Dapper.Contrib.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="DbConfiguration.cs" />
<Compile Include="Entity.cs" />
<Compile Include="Models\ScheduledJobs.cs" />
<Compile Include="Models\UsersToNotify.cs" />
<Compile Include="Repository\IRequestRepository.cs" />
<Compile Include="Repository\ISettingsRepository.cs" />
<Compile Include="ISqliteConfiguration.cs" />
@ -84,6 +82,7 @@
<DependentUpon>Sql.resx</DependentUpon>
</Compile>
<Compile Include="TableCreation.cs" />
<Compile Include="Models\Audit.cs" />
</ItemGroup>
<ItemGroup>
<None Include="sqlite3.dll">

@ -61,13 +61,7 @@ namespace PlexRequests.Store
}
[JsonIgnore]
public bool CanApprove
{
get
{
return !Approved && !Available;
}
}
public bool CanApprove => !Approved && !Available;
public bool UserHasRequested(string username)
{

@ -42,8 +42,34 @@ CREATE TABLE IF NOT EXISTS Logs
);
CREATE UNIQUE INDEX IF NOT EXISTS Logs_Id ON Logs (Id);
CREATE TABLE IF NOT EXISTS Audit
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Date varchar(100) NOT NULL,
Username varchar(100) NOT NULL,
ChangeType varchar(100) NOT NULL,
OldValue varchar(100),
NewValue varchar(100)
);
CREATE UNIQUE INDEX IF NOT EXISTS Audit_Id ON Audit (Id);
CREATE TABLE IF NOT EXISTS DBInfo
(
SchemaVersion INTEGER
);
CREATE TABLE IF NOT EXISTS ScheduledJobs
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name varchar(100) NOT NULL,
LastRun varchar(100) NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS ScheduledJobs_Id ON ScheduledJobs (Id);
CREATE TABLE IF NOT EXISTS UsersToNotify
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Username varchar(100) NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS UsersToNotify_Id ON UsersToNotify (Id);

@ -44,7 +44,18 @@ namespace PlexRequests.Store
connection.Close();
}
public static void AlterTable(IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType)
public static void DropTable(IDbConnection con, string tableName)
{
using (con)
{
con.Open();
var query = $"DROP TABLE IF EXISTS {tableName}";
con.Execute(query);
con.Close();
}
}
public static void AddColumn(IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType)
{
connection.Open();
var result = connection.Query<TableInfo>($"PRAGMA table_info({tableName});");
@ -83,7 +94,7 @@ namespace PlexRequests.Store
public static void CreateSchema(this IDbConnection con, int version)
{
con.Open();
con.Query(string.Format("INSERT INTO DBInfo (SchemaVersion) values ({0})", version));
con.Query($"INSERT INTO DBInfo (SchemaVersion) values ({version})");
con.Close();
}
@ -115,7 +126,5 @@ namespace PlexRequests.Store
public string dflt_value { get; set; }
public int pk { get; set; }
}
}
}

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Dapper" version="1.42" targetFramework="net452" />
<package id="Dapper.Contrib" version="1.43" targetFramework="net46" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net46" />
<package id="NLog" version="4.2.3" targetFramework="net452" />
<package id="Dapper" version="1.42" targetFramework="net45" />
<package id="Dapper.Contrib" version="1.43" targetFramework="net45" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net45" />
<package id="NLog" version="4.2.3" targetFramework="net45" />
</packages>

@ -25,6 +25,7 @@
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Linq;
using Moq;
@ -32,6 +33,7 @@ using Nancy;
using Nancy.Testing;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
@ -45,11 +47,11 @@ using PlexRequests.Store.Repository;
using PlexRequests.UI.Models;
using PlexRequests.UI.Modules;
using PlexRequests.Helpers;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Tests
{
[TestFixture]
[Ignore("Needs rework")]
public class AdminModuleTests
{
private Mock<ISettingsService<PlexRequestSettings>> PlexRequestMock { get; set; }
@ -70,6 +72,7 @@ namespace PlexRequests.UI.Tests
private Mock<IRepository<LogEntity>> LogRepo { get; set; }
private Mock<INotificationService> NotificationService { get; set; }
private Mock<ICacheProvider> Cache { get; set; }
private Mock<ISettingsService<LogSettings>> Log { get; set; }
private ConfigurableBootstrapper Bootstrapper { get; set; }
@ -82,9 +85,10 @@ namespace PlexRequests.UI.Tests
PlexMock = new Mock<IPlexApi>();
PlexMock.Setup(x => x.SignIn("Username1", "Password1"))
.Returns(new PlexAuthentication { user = new User { authentication_token = "abc", username = "Username1" } });
.Returns(new PlexAuthentication { user = new User { authentication_token = "abc", title = "Username1" } });
PlexRequestMock = new Mock<ISettingsService<PlexRequestSettings>>();
PlexRequestMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings());
CpMock = new Mock<ISettingsService<CouchPotatoSettings>>();
PlexSettingsMock = new Mock<ISettingsService<PlexSettings>>();
SonarrApiMock = new Mock<ISonarrApi>();
@ -100,6 +104,7 @@ namespace PlexRequests.UI.Tests
NotificationService = new Mock<INotificationService>();
HeadphonesSettings = new Mock<ISettingsService<HeadphonesSettings>>();
Cache = new Mock<ICacheProvider>();
Log = new Mock<ISettingsService<LogSettings>>();
Bootstrapper = new ConfigurableBootstrapper(with =>
{
@ -121,11 +126,12 @@ namespace PlexRequests.UI.Tests
with.Dependency(PushoverApi.Object);
with.Dependency(NotificationService.Object);
with.Dependency(HeadphonesSettings.Object);
with.Dependencies(Cache.Object);
with.Dependency(Cache.Object);
with.Dependency(Log.Object);
with.RootPathProvider<TestRootPathProvider>();
with.RequestStartup((container, pipelines, context) =>
{
context.CurrentUser = new UserIdentity { UserName = "user" };
context.CurrentUser = new UserIdentity { UserName = "user", Claims = new List<string> {"Admin"} };
});
});
@ -233,7 +239,7 @@ namespace PlexRequests.UI.Tests
[Test]
public void GetUsersSuccessfully()
{
var users = new PlexFriends { User = new[] { new UserFriends { Username = "abc2" }, } };
var users = new PlexFriends { User = new[] { new UserFriends { Title = "abc2" }, } };
PlexMock.Setup(x => x.GetUsers(It.IsAny<string>())).Returns(users);
var browser = new Browser(Bootstrapper);
@ -248,9 +254,11 @@ namespace PlexRequests.UI.Tests
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = result.Body.AsString();
var body = JsonConvert.DeserializeObject<JObject>(result.Body.AsString());
var user = body["users"];
Assert.That(body, Is.Not.Null);
Assert.That(body, Contains.Substring("abc2"));
Assert.That(user.ToString().Contains("abc"), Is.True);
PlexMock.Verify(x => x.GetUsers(It.IsAny<string>()), Times.Once);
AuthMock.Verify(x => x.GetSettings(), Times.Once);
@ -270,6 +278,7 @@ namespace PlexRequests.UI.Tests
with.FormValue("username", "Username1");
with.FormValue("password", "Password1");
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
@ -324,9 +333,10 @@ namespace PlexRequests.UI.Tests
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<string>(result.Body.AsString());
var body = JsonConvert.DeserializeObject<JObject>(result.Body.AsString());
var user = (string)body["users"];
Assert.That(body, Is.Not.Null);
Assert.That(string.IsNullOrWhiteSpace(body), Is.True);
Assert.That(string.IsNullOrWhiteSpace(user), Is.True);
PlexMock.Verify(x => x.GetUsers(It.IsAny<string>()), Times.Never);
AuthMock.Verify(x => x.GetSettings(), Times.Once);

@ -0,0 +1,778 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: ApiModuleTests.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 FluentValidation;
using Moq;
using Nancy;
using Nancy.Testing;
using Nancy.Validation;
using Nancy.Validation.FluentValidation;
using Newtonsoft.Json;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Models;
using PlexRequests.UI.Modules;
using PlexRequests.UI.Validators;
using Ploeh.AutoFixture;
namespace PlexRequests.UI.Tests
{
[TestFixture]
public class ApiModuleTests
{
private ConfigurableBootstrapper Bootstrapper { get; set; }
[SetUp]
public void Setup()
{
var fixture = new Fixture();
var requests = fixture.CreateMany<RequestedModel>();
var requestMock = new Mock<IRequestService>();
var settingsMock = new Mock<ISettingsService<PlexRequestSettings>>();
var userRepoMock = new Mock<IRepository<UsersModel>>();
var mapperMock = new Mock<ICustomUserMapper>();
var authSettingsMock = new Mock<ISettingsService<AuthenticationSettings>>();
var plexSettingsMock = new Mock<ISettingsService<PlexSettings>>();
var cpMock = new Mock<ISettingsService<CouchPotatoSettings>>();
var sonarrMock = new Mock<ISettingsService<SonarrSettings>>();
var sickRageMock = new Mock<ISettingsService<SickRageSettings>>();
var headphonesMock = new Mock<ISettingsService<HeadphonesSettings>>();
var userModels = fixture.CreateMany<UsersModel>().ToList();
userModels.Add(new UsersModel
{
UserName = "user1"
});
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings { ApiKey = "api" });
requestMock.Setup(x => x.GetAll()).Returns(requests);
requestMock.Setup(x => x.Get(1)).Returns(requests.FirstOrDefault());
requestMock.Setup(x => x.Get(99)).Returns(new RequestedModel());
requestMock.Setup(x => x.DeleteRequest(It.IsAny<RequestedModel>()));
userRepoMock.Setup(x => x.GetAll()).Returns(userModels);
userRepoMock.Setup(x => x.Update(It.IsAny<UsersModel>())).Returns(true);
mapperMock.Setup(x => x.ValidateUser("user1", It.IsAny<string>())).Returns(Guid.NewGuid());
mapperMock.Setup(x => x.UpdatePassword("user1", "password", "newpassword")).Returns(true);
authSettingsMock.Setup(x => x.SaveSettings(It.Is<AuthenticationSettings>(c => c.PlexAuthToken.Equals("abc")))).Returns(true);
plexSettingsMock.Setup(x => x.GetSettings()).Returns(fixture.Create<PlexSettings>());
plexSettingsMock.Setup(x => x.SaveSettings(It.Is<PlexSettings>(c => c.Ip.Equals("192")))).Returns(true);
cpMock.Setup(x => x.GetSettings()).Returns(fixture.Create<CouchPotatoSettings>());
cpMock.Setup(x => x.SaveSettings(It.Is<CouchPotatoSettings>(c => c.Ip.Equals("192")))).Returns(true);
sonarrMock.Setup(x => x.GetSettings()).Returns(fixture.Create<SonarrSettings>());
sonarrMock.Setup(x => x.SaveSettings(It.Is<SonarrSettings>(c => c.Ip.Equals("192")))).Returns(true);
sickRageMock.Setup(x => x.GetSettings()).Returns(fixture.Create<SickRageSettings>());
sickRageMock.Setup(x => x.SaveSettings(It.Is<SickRageSettings>(c => c.Ip.Equals("192")))).Returns(true);
headphonesMock.Setup(x => x.GetSettings()).Returns(fixture.Create<HeadphonesSettings>());
headphonesMock.Setup(x => x.SaveSettings(It.Is<HeadphonesSettings>(c => c.Ip.Equals("192")))).Returns(true);
Bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<ApiRequestModule>();
with.Module<ApiUserModule>();
with.Module<ApiSettingsModule>();
with.Dependency(requestMock.Object);
with.Dependency(settingsMock.Object);
with.Dependency(userRepoMock.Object);
with.Dependency(mapperMock.Object);
with.Dependency(headphonesMock.Object);
with.Dependency(authSettingsMock.Object);
with.Dependency(plexSettingsMock.Object);
with.Dependency(cpMock.Object);
with.Dependency(sonarrMock.Object);
with.Dependency(sickRageMock.Object);
with.RootPathProvider<TestRootPathProvider>();
with.ModelValidatorLocator(
new DefaultValidatorLocator(
new List<IModelValidatorFactory>
{
new FluentValidationValidatorFactory(
new DefaultFluentAdapterFactory(new List<IFluentAdapter>()),
new List<IValidator> { new RequestedModelValidator(), new UserViewModelValidator(), new PlexValidator() })
}));
});
}
private Action<BrowserContext> GetBrowser()
{
return with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
};
}
[Test]
public void InvalidApiKey()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/requests", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "a");
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<List<RequestedModel>>>(result.Body.AsString());
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.Not.Empty);
}
[Test]
public void GetAllRequests()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/requests", GetBrowser());
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<List<RequestedModel>>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null);
Assert.That(body.Data.Count, Is.GreaterThan(0));
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null.Or.Empty);
}
[Test]
public void GetSingleRequest()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/requests/1", GetBrowser());
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<List<RequestedModel>>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null);
Assert.That(body.Data.Count, Is.EqualTo(1));
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null.Or.Empty);
}
[Test]
public void GetSingleRequestThatDoesntExist()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/requests/99", GetBrowser());
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<List<RequestedModel>>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null);
Assert.That(body.Data.Count, Is.EqualTo(0));
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty);
}
[Test]
public void DeleteARequest()
{
var browser = new Browser(Bootstrapper);
var result = browser.Delete("/api/requests/1", GetBrowser());
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.True);
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null.Or.Empty);
}
[Test]
public void DeleteARequestThatDoesNotExist()
{
var browser = new Browser(Bootstrapper);
var result = browser.Delete("/api/requests/99", GetBrowser());
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.False);
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty);
}
[Test]
public void UpdateUsersPassword()
{
var model = new UserUpdateViewModel
{
CurrentPassword = "password",
NewPassword = "newpassword"
};
var browser = new Browser(Bootstrapper);
var result = browser.Put("/api/credentials/user1", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<string>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null.Or.Empty);
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null.Or.Empty);
}
[Test]
public void UpdateInvalidUsersPassword()
{
var model = new UserUpdateViewModel
{
CurrentPassword = "password",
NewPassword = "newpassword"
};
var browser = new Browser(Bootstrapper);
var result = browser.Put("/api/credentials/user99", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<string>>(result.Body.AsString());
Assert.That(body.Data, Is.Null.Or.Empty);
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty);
}
[Test]
public void UpdateUsersInvalidPassword()
{
var model = new UserUpdateViewModel
{
CurrentPassword = "password",
NewPassword = "password2"
};
var browser = new Browser(Bootstrapper);
var result = browser.Put("/api/credentials/user1", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<string>>(result.Body.AsString());
Assert.That(body.Data, Is.Null.Or.Empty);
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty);
}
[Test]
public void UpdateUsersWithBadModel()
{
var model = new UserUpdateViewModel
{
CurrentPassword = null,
NewPassword = "password2"
};
var browser = new Browser(Bootstrapper);
var result = browser.Put("/api/credentials/user1", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<string[]>>(result.Body.AsString());
Assert.That(body.Data.Length, Is.GreaterThan(0));
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty);
}
[Test]
public void GetApiKey()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/apikey", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.Query("username", "user1");
with.Query("password", "password");
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<string>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null.Or.Empty);
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null.Or.Empty);
}
[Test]
public void GetApiKeyWithBadCredentials()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/apikey", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.Query("username", "user");
with.Query("password", "password");
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<string>>(result.Body.AsString());
Assert.That(body.Data, Is.Null.Or.Empty);
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.Not.Null.Or.Empty);
}
[Test]
public void SaveNewAuthSettings()
{
var model = new AuthenticationSettings
{
Id = 1,
PlexAuthToken = "abc",
DeniedUsers = "abc",
UsePassword = false,
UserAuthentication = true
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("api/settings/authentication", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<string>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null.Or.Empty);
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null.Or.Empty);
}
[Test]
public void GetPlexSettings()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/settings/plex", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
});
var body = JsonConvert.DeserializeObject<ApiModel<PlexSettings>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null);
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SavePlexSettings()
{
var model = new PlexSettings()
{
Port = 231,
Ip = "192",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/plex", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(true));
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SaveBadPlexSettings()
{
var model = new PlexSettings
{
Ip = "q",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/plex", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(false));
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.EqualTo("Could not update the settings"));
}
[Test]
public void GetCpSettings()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/settings/couchpotato", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
});
var body = JsonConvert.DeserializeObject<ApiModel<CouchPotatoSettings>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null);
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SaveCpSettings()
{
var model = new CouchPotatoSettings
{
Port = 231,
Ip = "192",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/couchpotato", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(true));
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SaveBadCpSettings()
{
var model = new CouchPotatoSettings
{
Ip = "q",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/couchpotato", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(false));
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.EqualTo("Could not update the settings"));
}
[Test]
public void GetSonarrSettings()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/settings/sonarr", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
});
var body = JsonConvert.DeserializeObject<ApiModel<SonarrSettings>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null);
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SaveSonarrSettings()
{
var model = new SonarrSettings
{
Port = 231,
Ip = "192",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/sonarr", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(true));
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SaveBadSonarrSettings()
{
var model = new SonarrSettings
{
Ip = "q",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/sonarr", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(false));
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.EqualTo("Could not update the settings"));
}
[Test]
public void GetSickRageSettings()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/settings/sickrage", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
});
var body = JsonConvert.DeserializeObject<ApiModel<SickRageSettings>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null);
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SaveSickRageSettings()
{
var model = new SickRageSettings
{
Port = 231,
Ip = "192",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/sickrage", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(true));
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SaveBadSickRageSettings()
{
var model = new SickRageSettings
{
Ip = "q",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/sickrage", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(false));
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.EqualTo("Could not update the settings"));
}
[Test]
public void GetHeadphonesSettings()
{
var browser = new Browser(Bootstrapper);
var result = browser.Get("/api/settings/headphones", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
});
var body = JsonConvert.DeserializeObject<ApiModel<HeadphonesSettings>>(result.Body.AsString());
Assert.That(body.Data, Is.Not.Null);
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SaveHeadphonesSettings()
{
var model = new HeadphonesSettings
{
Port = 231,
Ip = "192",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/headphones", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(true));
Assert.That(body.Error, Is.False);
Assert.That(body.ErrorMessage, Is.Null);
}
[Test]
public void SaveBadHeadphonesSettings()
{
var model = new HeadphonesSettings
{
Ip = "q",
Ssl = true,
};
var browser = new Browser(Bootstrapper);
var result = browser.Post("/api/settings/headphones", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
Assert.That(body.Data, Is.EqualTo(false));
Assert.That(body.Error, Is.True);
Assert.That(body.ErrorMessage, Is.EqualTo("Could not update the settings"));
}
[TestCaseSource(nameof(AuthSettingsData))]
public object SaveNewAuthSettings(object model)
{
var browser = new Browser(Bootstrapper);
var result = browser.Post("api/settings/authentication", with =>
{
with.HttpRequest();
with.Header("Accept", "application/json");
with.Query("apikey", "api");
with.JsonBody(model);
});
Assert.That(HttpStatusCode.OK, Is.EqualTo(result.StatusCode));
var body = JsonConvert.DeserializeObject<ApiModel<bool>>(result.Body.AsString());
var retVal = new List<string> { body.ErrorMessage, body.Error.ToString(), body.Data.ToString() };
return retVal;
}
private static IEnumerable<TestCaseData> AuthSettingsData
{
get
{
yield return
new TestCaseData(new AuthenticationSettings { Id = 1, PlexAuthToken = "abc", DeniedUsers = "abc", UsePassword = false, UserAuthentication = true })
.Returns(new List<string> { null, false.ToString(), true.ToString() });
}
}
}
}

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
@ -46,6 +46,11 @@
<HintPath>..\packages\FluentScheduler.3.1.46\lib\net40\FluentScheduler.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="FluentValidation, Version=6.2.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Moq, Version=4.2.1510.2205, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll</HintPath>
<Private>True</Private>
@ -58,6 +63,10 @@
<HintPath>..\packages\Nancy.Testing.1.4.1\lib\net40\Nancy.Testing.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Nancy.Validation.FluentValidation, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nancy.Validation.FluentValidation.1.4.1\lib\net40\Nancy.Validation.FluentValidation.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Nancy.ViewEngines.Razor, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nancy.Viewengines.Razor.1.4.3\lib\net40\Nancy.ViewEngines.Razor.dll</HintPath>
<Private>True</Private>
@ -90,6 +99,7 @@
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="ApiModuleTests.cs" />
<Compile Include="BootstrapperExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestRootPathProvider.cs" />

@ -30,7 +30,6 @@ using Moq;
using Nancy;
using Nancy.Testing;
using Nancy.TinyIoc;
using Newtonsoft.Json;
@ -40,18 +39,18 @@ using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using PlexRequests.UI.Modules;
namespace PlexRequests.UI.Tests
{
[TestFixture]
[Ignore("Needs some work")]
//[Ignore("Needs some work")]
public class UserLoginModuleTests
{
private Mock<ISettingsService<AuthenticationSettings>> AuthMock { get; set; }
private Mock<ISettingsService<PlexRequestSettings>> PlexRequestMock { get; set; }
private ConfigurableBootstrapper Bootstrapper { get; set; }
private Mock<IPlexApi> PlexMock { get; set; }
[SetUp]
@ -60,6 +59,15 @@ namespace PlexRequests.UI.Tests
AuthMock = new Mock<ISettingsService<AuthenticationSettings>>();
PlexMock = new Mock<IPlexApi>();
PlexRequestMock = new Mock<ISettingsService<PlexRequestSettings>>();
PlexRequestMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings());
Bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(PlexRequestMock.Object);
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
}
[Test]
@ -68,18 +76,11 @@ namespace PlexRequests.UI.Tests
var expectedSettings = new AuthenticationSettings { UserAuthentication = false, PlexAuthToken = "abc" };
AuthMock.Setup(x => x.GetSettings()).Returns(expectedSettings);
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.Dependency(PlexRequestMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(bootstrapper);
Bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(Bootstrapper);
var result = browser.Post("/userlogin", with =>
{
with.HttpRequest();
@ -103,17 +104,10 @@ namespace PlexRequests.UI.Tests
var expectedSettings = new AuthenticationSettings { UserAuthentication = false, PlexAuthToken = "abc" };
AuthMock.Setup(x => x.GetSettings()).Returns(expectedSettings);
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
bootstrapper.WithSession(new Dictionary<string, object>());
Bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(bootstrapper);
var browser = new Browser(Bootstrapper);
var result = browser.Post("/userlogin", with =>
{
with.HttpRequest();
@ -140,7 +134,7 @@ namespace PlexRequests.UI.Tests
{
new UserFriends
{
Username = "abc",
Title = "abc",
},
}
};
@ -149,17 +143,9 @@ namespace PlexRequests.UI.Tests
PlexMock.Setup(x => x.GetUsers(It.IsAny<string>())).Returns(plexFriends);
PlexMock.Setup(x => x.GetAccount(It.IsAny<string>())).Returns(new PlexAccount());
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
bootstrapper.WithSession(new Dictionary<string, object>());
Bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(bootstrapper);
var browser = new Browser(Bootstrapper);
var result = browser.Post("/userlogin", with =>
{
with.HttpRequest();
@ -196,17 +182,9 @@ namespace PlexRequests.UI.Tests
PlexMock.Setup(x => x.GetUsers(It.IsAny<string>())).Returns(plexFriends);
PlexMock.Setup(x => x.GetAccount(It.IsAny<string>())).Returns(new PlexAccount());
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
Bootstrapper.WithSession(new Dictionary<string, object>());
bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(bootstrapper);
var browser = new Browser(Bootstrapper);
var result = browser.Post("/userlogin", with =>
{
@ -237,7 +215,7 @@ namespace PlexRequests.UI.Tests
{
new UserFriends
{
Username = "abc",
Title = "abc",
}
}
};
@ -254,17 +232,9 @@ namespace PlexRequests.UI.Tests
PlexMock.Setup(x => x.SignIn(It.IsAny<string>(), It.IsAny<string>())).Returns(plexAuth);
PlexMock.Setup(x => x.GetAccount(It.IsAny<string>())).Returns(new PlexAccount());
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
bootstrapper.WithSession(new Dictionary<string, object>());
Bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(bootstrapper);
var browser = new Browser(Bootstrapper);
var result = browser.Post("/userlogin", with =>
{
with.HttpRequest();
@ -307,17 +277,10 @@ namespace PlexRequests.UI.Tests
PlexMock.Setup(x => x.GetUsers(It.IsAny<string>())).Returns(plexFriends);
PlexMock.Setup(x => x.SignIn(It.IsAny<string>(), It.IsAny<string>())).Returns(plexAuth);
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
bootstrapper.WithSession(new Dictionary<string, object>());
Bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(bootstrapper);
var browser = new Browser(Bootstrapper);
var result = browser.Post("/userlogin", with =>
{
with.HttpRequest();
@ -344,17 +307,9 @@ namespace PlexRequests.UI.Tests
var expectedSettings = new AuthenticationSettings { UserAuthentication = false, DeniedUsers = "abc", PlexAuthToken = "abc" };
AuthMock.Setup(x => x.GetSettings()).Returns(expectedSettings);
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
bootstrapper.WithSession(new Dictionary<string, object>());
Bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(bootstrapper);
var browser = new Browser(Bootstrapper);
var result = browser.Post("/userlogin", with =>
{
with.HttpRequest();
@ -376,17 +331,9 @@ namespace PlexRequests.UI.Tests
[Test]
public void Logout()
{
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
bootstrapper.WithSession(new Dictionary<string, object> { { SessionKeys.UsernameKey, "abc" } });
Bootstrapper.WithSession(new Dictionary<string, object> { { SessionKeys.UsernameKey, "abc" } });
var browser = new Browser(bootstrapper);
var browser = new Browser(Bootstrapper);
var result = browser.Get("/userlogin/logout", with =>
{
with.HttpRequest();
@ -415,17 +362,9 @@ namespace PlexRequests.UI.Tests
PlexMock.Setup(x => x.GetAccount(It.IsAny<string>())).Returns(account);
PlexMock.Setup(x => x.SignIn(It.IsAny<string>(), It.IsAny<string>())).Returns(new PlexAuthentication { user = new User { username = "Jamie" } });
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
bootstrapper.WithSession(new Dictionary<string, object>());
Bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(bootstrapper);
var browser = new Browser(Bootstrapper);
var result = browser.Post("/userlogin", with =>
{
with.HttpRequest();
@ -470,17 +409,9 @@ namespace PlexRequests.UI.Tests
PlexMock.Setup(x => x.SignIn(It.IsAny<string>(), It.IsAny<string>())).Returns(plexAuth);
PlexMock.Setup(x => x.GetAccount(It.IsAny<string>())).Returns(account);
var bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
bootstrapper.WithSession(new Dictionary<string, object>());
Bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(bootstrapper);
var browser = new Browser(Bootstrapper);
var result = browser.Post("/userlogin", with =>
{
with.HttpRequest();

@ -3,10 +3,12 @@
<package id="AutoFixture" version="3.40.0" targetFramework="net452" />
<package id="CsQuery" version="1.3.3" targetFramework="net46" />
<package id="FluentScheduler" version="3.1.46" targetFramework="net46" />
<package id="FluentValidation" version="6.2.1.0" targetFramework="net46" />
<package id="Microsoft.AspNet.Razor" version="2.0.30506.0" targetFramework="net46" />
<package id="Moq" version="4.2.1510.2205" targetFramework="net452" />
<package id="Nancy" version="1.4.3" targetFramework="net46" />
<package id="Nancy.Testing" version="1.4.1" targetFramework="net46" />
<package id="Nancy.Validation.FluentValidation" version="1.4.1" targetFramework="net46" />
<package id="Nancy.Viewengines.Razor" version="1.4.3" targetFramework="net46" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net46" />
<package id="NUnit" version="3.2.0" targetFramework="net46" />

@ -50,6 +50,12 @@ using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using Nancy.Json;
using PlexRequests.Services.Jobs;
using PlexRequests.UI.Jobs;
using Quartz.Spi;
namespace PlexRequests.UI
{
@ -62,8 +68,7 @@ namespace PlexRequests.UI
protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context)
{
container.Register<IUserMapper, UserMapper>();
container.Register<ISqliteConfiguration, DbConfiguration>(new DbConfiguration(new SqliteFactory()));
container.Register<ICacheProvider, MemoryCacheProvider>().AsSingleton();
// Settings
@ -77,19 +82,23 @@ namespace PlexRequests.UI
container.Register<ISettingsService<PushbulletNotificationSettings>, SettingsServiceV2<PushbulletNotificationSettings>>();
container.Register<ISettingsService<PushoverNotificationSettings>, SettingsServiceV2<PushoverNotificationSettings>>();
container.Register<ISettingsService<HeadphonesSettings>, SettingsServiceV2<HeadphonesSettings>>();
container.Register<ISettingsService<LogSettings>, SettingsServiceV2<LogSettings>>();
container.Register<ISettingsService<SlackNotificationSettings>, SettingsServiceV2<SlackNotificationSettings>>();
// Repo's
container.Register<IRepository<LogEntity>, GenericRepository<LogEntity>>();
container.Register<IRepository<UsersToNotify>, GenericRepository<UsersToNotify>>();
container.Register<IRepository<ScheduledJobs>, GenericRepository<ScheduledJobs>>();
container.Register<IRequestService, JsonRequestService>();
container.Register<ISettingsRepository, SettingsJsonRepository>();
container.Register<IJobRecord, JobRecord>();
// Services
container.Register<IAvailabilityChecker, PlexAvailabilityChecker>();
container.Register<ICouchPotatoCacher, CouchPotatoCacher>();
container.Register<ISonarrCacher, SonarrCacher>();
container.Register<ISickRageCacher, SickRageCacher>();
container.Register<IConfigurationReader, ConfigurationReader>();
container.Register<IIntervals, UpdateInterval>();
container.Register<IJobFactory, CustomJobFactory>();
// Api's
container.Register<ICouchPotatoApi, CouchPotatoApi>();
@ -100,18 +109,28 @@ namespace PlexRequests.UI
container.Register<IPlexApi, PlexApi>();
container.Register<IMusicBrainzApi, MusicBrainzApi>();
container.Register<IHeadphonesApi, HeadphonesApi>();
container.Register<ISlackApi, SlackApi>();
// NotificationService
container.Register<INotificationService, NotificationService>().AsSingleton();
JsonSettings.MaxJsonLength = int.MaxValue;
SubscribeAllObservers(container);
base.ConfigureRequestContainer(container, context);
var loc = ServiceLocator.Instance;
loc.SetContainer(container);
}
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
container.Register<ISqliteConfiguration, DbConfiguration>(new DbConfiguration(new SqliteFactory()));
container.Register<IRepository<UsersModel>, UserRepository<UsersModel>>();
container.Register<IUserMapper, UserMapper>();
container.Register<ICustomUserMapper, UserMapper>();
CookieBasedSessions.Enable(pipelines, CryptographyConfiguration.Default);
StaticConfiguration.DisableErrorTraces = false;
@ -146,6 +165,8 @@ namespace PlexRequests.UI
nancyConventions.StaticContentsConventions.Add(
StaticContentConventionBuilder.AddDirectory($"{assetLocation}/Content", "Content")
);
nancyConventions.StaticContentsConventions.AddDirectory($"{assetLocation}/docs", "swagger-ui");
}
protected override DiagnosticsConfiguration DiagnosticsConfiguration => new DiagnosticsConfiguration { Password = @"password" };
@ -174,6 +195,26 @@ namespace PlexRequests.UI
{
notificationService.Subscribe(new PushoverNotification(container.Resolve<IPushoverApi>(), pushoverService));
}
var slackService = container.Resolve<ISettingsService<SlackNotificationSettings>>();
var slackSettings = slackService.GetSettings();
if (slackSettings.Enabled)
{
notificationService.Subscribe(new SlackNotification(container.Resolve<ISlackApi>(), slackService));
}
}
protected override void RequestStartup(TinyIoCContainer container, IPipelines pipelines, NancyContext context)
{
//CORS Enable
pipelines.AfterRequest.AddItemToEndOfPipeline((ctx) =>
{
ctx.Response.WithHeader("Access-Control-Allow-Origin", "*")
.WithHeader("Access-Control-Allow-Methods", "POST,GET")
.WithHeader("Access-Control-Allow-Headers", "Accept, Origin, Content-type");
});
base.RequestStartup(container, pipelines, context);
}
}
}

@ -42,11 +42,28 @@ label {
margin-bottom: 0.5rem !important;
font-size: 16px !important; }
.nav-tabs > li {
font-size: 13px;
line-height: 21px; }
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:hover,
.nav-tabs > li.active > a:focus {
background: #4e5d6c; }
.nav-tabs > li > a > .fa {
padding: 3px 5px 3px 3px; }
.nav-tabs > li.nav-tab-right {
float: right; }
.nav-tabs > li.nav-tab-right a {
margin-right: 0;
margin-left: 2px; }
.nav-tabs > li.nav-tab-icononly .fa {
padding: 3px; }
.navbar .nav a .fa,
.dropdown-menu a .fa {
font-size: 130%;
@ -220,3 +237,47 @@ label {
border-radius: 0 0.25rem 0.25rem 0 !important;
padding: 12px 8px; }
#updateAvailable {
background-color: #ffa400;
text-align: center;
font-size: 15px; }
.checkbox label {
display: inline-block;
cursor: pointer;
position: relative;
padding-left: 25px;
margin-right: 15px;
font-size: 13px;
margin-bottom: 10px; }
.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: 3px; }
.checkbox input[type=checkbox] {
display: none; }
.checkbox input[type=checkbox]:checked + label:before {
content: "\2713";
font-size: 13px;
color: #fafafa;
text-align: center;
line-height: 13px; }
.input-group-sm {
padding-top: 2px;
padding-bottom: 2px; }
.tab-pane .form-horizontal .form-group {
margin-right: 15px;
margin-left: 15px; }

File diff suppressed because one or more lines are too long

@ -7,8 +7,7 @@ $warning-colour: #f0ad4e;
$danger-colour: #d9534f;
$success-colour: #5cb85c;
$i:
!important
;
!important;
@media (min-width: 768px ) {
.row {
@ -70,12 +69,34 @@ label {
font-size: 16px $i;
}
.nav-tabs > li {
font-size: 13px;
line-height: 21px;
}
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:hover,
.nav-tabs > li.active > a:focus {
background: #4e5d6c;
}
.nav-tabs > li > a > .fa {
padding: 3px 5px 3px 3px;
}
.nav-tabs > li.nav-tab-right {
float: right;
}
.nav-tabs > li.nav-tab-right a {
margin-right: 0;
margin-left: 2px;
}
.nav-tabs > li.nav-tab-icononly .fa {
padding: 3px;
}
.navbar .nav a .fa,
.dropdown-menu a .fa {
font-size: 130%;
@ -280,3 +301,49 @@ $border-radius: 10px;
border-radius: 0 .25rem .25rem 0 $i;
padding: 12px 8px;
}
#updateAvailable {
background-color: rgb(255, 164, 0);
text-align: center;
font-size: 15px;
}
.checkbox label {
display: inline-block;
cursor: pointer;
position: relative;
padding-left: 25px;
margin-right: 15px;
font-size: 13px;
margin-bottom: 10px; }
.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: 3px; }
.checkbox input[type=checkbox] {
display: none; }
.checkbox input[type=checkbox]:checked + label:before {
content: "\2713";
font-size: 13px;
color: #fafafa;
text-align: center;
line-height: 13px; }
.input-group-sm{
padding-top: 2px;
padding-bottom: 2px;
}
.tab-pane .form-horizontal .form-group {
margin-right: 15px;
margin-left: 15px; }

File diff suppressed because it is too large Load Diff

@ -0,0 +1,283 @@
@media (min-width: 768px) {
.row {
position: relative; }
.bottom-align-text {
position: absolute;
bottom: 0;
right: 0; } }
@media (max-width: 48em) {
.home {
padding-top: 1rem; } }
@media (min-width: 48em) {
.home {
padding-top: 4rem; } }
.btn {
border-radius: 0.25rem !important; }
.multiSelect {
background-color: #4e5d6c; }
.form-control-custom {
background-color: #333333 !important;
color: white !important;
border-radius: 0;
box-shadow: 0 0 0 !important; }
h1 {
font-size: 3.5rem !important;
font-weight: 600 !important; }
.request-title {
margin-top: 0 !important;
font-size: 1.9rem !important; }
p {
font-size: 1.1rem !important; }
label {
display: inline-block !important;
margin-bottom: 0.5rem !important;
font-size: 16px !important; }
.nav-tabs > li {
font-size: 13px;
line-height: 21px; }
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:hover,
.nav-tabs > li.active > a:focus {
background: #df691a; }
.nav-tabs > li > a > .fa {
padding: 3px 5px 3px 3px; }
.nav-tabs > li.nav-tab-right {
float: right; }
.nav-tabs > li.nav-tab-right a {
margin-right: 0;
margin-left: 2px; }
.nav-tabs > li.nav-tab-icononly .fa {
padding: 3px; }
.navbar .nav a .fa,
.dropdown-menu a .fa {
font-size: 130%;
top: 1px;
position: relative;
display: inline-block;
margin-right: 5px; }
.dropdown-menu a .fa {
top: 2px; }
.btn-danger-outline {
color: #d9534f !important;
background-color: transparent;
background-image: none;
border-color: #d9534f !important; }
.btn-danger-outline:focus,
.btn-danger-outline.focus,
.btn-danger-outline:active,
.btn-danger-outline.active,
.btn-danger-outline:hover,
.open > .btn-danger-outline.dropdown-toggle {
color: #fff !important;
background-color: #d9534f !important;
border-color: #d9534f !important; }
.btn-primary-outline {
color: #ff761b !important;
background-color: transparent;
background-image: none;
border-color: #ff761b !important; }
.btn-primary-outline:focus,
.btn-primary-outline.focus,
.btn-primary-outline:active,
.btn-primary-outline.active,
.btn-primary-outline:hover,
.open > .btn-primary-outline.dropdown-toggle {
color: #fff !important;
background-color: #df691a !important;
border-color: #df691a !important; }
.btn-info-outline {
color: #5bc0de !important;
background-color: transparent;
background-image: none;
border-color: #5bc0de !important; }
.btn-info-outline:focus,
.btn-info-outline.focus,
.btn-info-outline:active,
.btn-info-outline.active,
.btn-info-outline:hover,
.open > .btn-info-outline.dropdown-toggle {
color: #fff !important;
background-color: #5bc0de !important;
border-color: #5bc0de !important; }
.btn-warning-outline {
color: #f0ad4e !important;
background-color: transparent;
background-image: none;
border-color: #f0ad4e !important; }
.btn-warning-outline:focus,
.btn-warning-outline.focus,
.btn-warning-outline:active,
.btn-warning-outline.active,
.btn-warning-outline:hover,
.open > .btn-warning-outline.dropdown-toggle {
color: #fff !important;
background-color: #f0ad4e !important;
border-color: #f0ad4e !important; }
.btn-success-outline {
color: #5cb85c !important;
background-color: transparent;
background-image: none;
border-color: #5cb85c !important; }
.btn-success-outline:focus,
.btn-success-outline.focus,
.btn-success-outline:active,
.btn-success-outline.active,
.btn-success-outline:hover,
.open > .btn-success-outline.dropdown-toggle {
color: #fff !important;
background-color: #5cb85c !important;
border-color: #5cb85c !important; }
#movieList .mix {
display: none; }
#tvList .mix {
display: none; }
.scroll-top-wrapper {
position: fixed;
opacity: 0;
visibility: hidden;
overflow: hidden;
text-align: center;
z-index: 99999999;
background-color: #333333;
color: #eeeeee;
width: 50px;
height: 48px;
line-height: 48px;
right: 30px;
bottom: 30px;
padding-top: 2px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
-webkit-transition: all 0.5s ease-in-out;
-moz-transition: all 0.5s ease-in-out;
-ms-transition: all 0.5s ease-in-out;
-o-transition: all 0.5s ease-in-out;
transition: all 0.5s ease-in-out; }
.scroll-top-wrapper:hover {
background-color: #df691a; }
.scroll-top-wrapper.show {
visibility: visible;
cursor: pointer;
opacity: 1.0; }
.scroll-top-wrapper i.fa {
line-height: inherit; }
.no-search-results {
text-align: center; }
.no-search-results .no-search-results-icon {
font-size: 10em;
color: #4e5d6c; }
.no-search-results .no-search-results-text {
margin: 20px 0;
color: #ccc; }
.form-control-search {
padding: 13px 105px 13px 16px;
height: 100%; }
.form-control-withbuttons {
padding-right: 105px; }
.input-group-addon .btn-group {
position: absolute;
right: 45px;
z-index: 3;
top: 13px;
box-shadow: 0 0 0; }
.input-group-addon .btn-group .btn {
border: 1px solid rgba(255, 255, 255, 0.7) !important;
padding: 3px 12px;
color: rgba(255, 255, 255, 0.7) !important; }
.btn-split .btn {
border-radius: 0 !important; }
.btn-split .btn:not(.dropdown-toggle) {
border-radius: 0.25rem 0 0 0.25rem !important; }
.btn-split .btn.dropdown-toggle {
border-radius: 0 0.25rem 0.25rem 0 !important;
padding: 12px 8px; }
#updateAvailable {
background-color: #ffa400;
text-align: center;
font-size: 15px; }
.checkbox label {
display: inline-block;
cursor: pointer;
position: relative;
padding-left: 25px;
margin-right: 15px;
font-size: 13px;
margin-bottom: 10px; }
.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: 3px; }
.checkbox input[type=checkbox] {
display: none; }
.checkbox input[type=checkbox]:checked + label:before {
content: "\2713";
font-size: 13px;
color: #fafafa;
text-align: center;
line-height: 13px; }
.input-group-sm {
padding-top: 2px;
padding-bottom: 2px; }
.tab-pane .form-horizontal .form-group {
margin-right: 15px;
margin-left: 15px; }

File diff suppressed because one or more lines are too long

@ -0,0 +1,350 @@
$form-color: #4e5d6c;
$form-color-lighter: #637689;
$primary-colour: #df691a;
$primary-colour-outline: #ff761b;
$info-colour: #5bc0de;
$warning-colour: #f0ad4e;
$danger-colour: #d9534f;
$success-colour: #5cb85c;
$bg-colour: #333333;
$i:
!important;
@media (min-width: 768px ) {
.row {
position: relative;
}
.bottom-align-text {
position: absolute;
bottom: 0;
right: 0;
}
}
@media (max-width: 48em) {
.home {
padding-top: 1rem;
}
}
@media (min-width: 48em) {
.home {
padding-top: 4rem;
}
}
.btn {
border-radius: .25rem $i;
}
.multiSelect {
background-color: $form-color;
}
.form-control-custom {
background-color: $bg-colour $i;
color: white $i;
border-radius: 0;
box-shadow: 0 0 0 !important;
}
h1 {
font-size: 3.5rem $i;
font-weight: 600 $i;
}
.request-title {
margin-top: 0 $i;
font-size: 1.9rem $i;
}
p {
font-size: 1.1rem $i;
}
label {
display: inline-block $i;
margin-bottom: .5rem $i;
font-size: 16px $i;
}
.nav-tabs > li {
font-size: 13px;
line-height: 21px;
}
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:hover,
.nav-tabs > li.active > a:focus {
background: $primary-colour;
}
.nav-tabs > li > a > .fa {
padding: 3px 5px 3px 3px;
}
.nav-tabs > li.nav-tab-right {
float: right;
}
.nav-tabs > li.nav-tab-right a {
margin-right: 0;
margin-left: 2px;
}
.nav-tabs > li.nav-tab-icononly .fa {
padding: 3px;
}
.navbar .nav a .fa,
.dropdown-menu a .fa {
font-size: 130%;
top: 1px;
position: relative;
display: inline-block;
margin-right: 5px;
}
.dropdown-menu a .fa {
top: 2px;
}
.btn-danger-outline {
color: $danger-colour $i;
background-color: transparent;
background-image: none;
border-color: $danger-colour $i;
}
.btn-danger-outline:focus,
.btn-danger-outline.focus,
.btn-danger-outline:active,
.btn-danger-outline.active,
.btn-danger-outline:hover,
.open > .btn-danger-outline.dropdown-toggle {
color: #fff $i;
background-color: $danger-colour $i;
border-color: $danger-colour $i;
}
.btn-primary-outline {
color: $primary-colour-outline $i;
background-color: transparent;
background-image: none;
border-color: $primary-colour-outline $i;
}
.btn-primary-outline:focus,
.btn-primary-outline.focus,
.btn-primary-outline:active,
.btn-primary-outline.active,
.btn-primary-outline:hover,
.open > .btn-primary-outline.dropdown-toggle {
color: #fff $i;
background-color: $primary-colour $i;
border-color: $primary-colour $i;
}
.btn-info-outline {
color: $info-colour $i;
background-color: transparent;
background-image: none;
border-color: $info-colour $i;
}
.btn-info-outline:focus,
.btn-info-outline.focus,
.btn-info-outline:active,
.btn-info-outline.active,
.btn-info-outline:hover,
.open > .btn-info-outline.dropdown-toggle {
color: #fff $i;
background-color: $info-colour $i;
border-color: $info-colour $i;
}
.btn-warning-outline {
color: $warning-colour $i;
background-color: transparent;
background-image: none;
border-color: $warning-colour $i;
}
.btn-warning-outline:focus,
.btn-warning-outline.focus,
.btn-warning-outline:active,
.btn-warning-outline.active,
.btn-warning-outline:hover,
.open > .btn-warning-outline.dropdown-toggle {
color: #fff $i;
background-color: $warning-colour $i;
border-color: $warning-colour $i;
}
.btn-success-outline {
color: $success-colour $i;
background-color: transparent;
background-image: none;
border-color: $success-colour $i;
}
.btn-success-outline:focus,
.btn-success-outline.focus,
.btn-success-outline:active,
.btn-success-outline.active,
.btn-success-outline:hover,
.open > .btn-success-outline.dropdown-toggle {
color: #fff $i;
background-color: $success-colour $i;
border-color: $success-colour $i;
}
#movieList .mix {
display: none;
}
#tvList .mix {
display: none;
}
$border-radius: 10px;
.scroll-top-wrapper {
position: fixed;
opacity: 0;
visibility: hidden;
overflow: hidden;
text-align: center;
z-index: 99999999;
background-color: $bg-colour;
color: #eeeeee;
width: 50px;
height: 48px;
line-height: 48px;
right: 30px;
bottom: 30px;
padding-top: 2px;
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
border-bottom-left-radius: $border-radius;
-webkit-transition: all 0.5s ease-in-out;
-moz-transition: all 0.5s ease-in-out;
-ms-transition: all 0.5s ease-in-out;
-o-transition: all 0.5s ease-in-out;
transition: all 0.5s ease-in-out;
}
.scroll-top-wrapper:hover {
background-color: $primary-colour;
}
.scroll-top-wrapper.show {
visibility: visible;
cursor: pointer;
opacity: 1.0;
}
.scroll-top-wrapper i.fa {
line-height: inherit;
}
.no-search-results {
text-align: center;
}
.no-search-results .no-search-results-icon {
font-size: 10em;
color: $form-color;
}
.no-search-results .no-search-results-text {
margin: 20px 0;
color: #ccc;
}
.form-control-search {
padding: 13px 105px 13px 16px;
height: 100%;
}
.form-control-withbuttons {
padding-right: 105px;
}
.input-group-addon .btn-group {
position: absolute;
right: 45px;
z-index: 3;
top: 13px;
box-shadow: 0 0 0;
}
.input-group-addon .btn-group .btn {
border: 1px solid rgba(255,255,255,.7) !important;
padding: 3px 12px;
color: rgba(255,255,255,.7) !important;
}
.btn-split .btn {
border-radius: 0 !important;
}
.btn-split .btn:not(.dropdown-toggle) {
border-radius: .25rem 0 0 .25rem $i;
}
.btn-split .btn.dropdown-toggle {
border-radius: 0 .25rem .25rem 0 $i;
padding: 12px 8px;
}
#updateAvailable {
background-color: rgb(255, 164, 0);
text-align: center;
font-size: 15px;
}
.checkbox label {
display: inline-block;
cursor: pointer;
position: relative;
padding-left: 25px;
margin-right: 15px;
font-size: 13px;
margin-bottom: 10px; }
.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: 3px; }
.checkbox input[type=checkbox] {
display: none; }
.checkbox input[type=checkbox]:checked + label:before {
content: "\2713";
font-size: 13px;
color: #fafafa;
text-align: center;
line-height: 13px; }
.input-group-sm{
padding-top: 2px;
padding-bottom: 2px;
}
.tab-pane .form-horizontal .form-group {
margin-right: 15px;
margin-left: 15px; }

@ -0,0 +1,266 @@
@charset "UTF-8";
.abc-checkbox {
padding-left: 20px; }
.abc-checkbox label {
display: inline-block;
vertical-align: middle;
position: relative;
padding-left: 5px; }
.abc-checkbox label::before {
cursor: pointer;
content: "";
display: inline-block;
position: absolute;
width: 17px;
height: 17px;
left: 0;
margin-left: -20px;
border: 1px solid #ccc;
border-radius: 3px;
background-color: #fff; }
.abc-checkbox label::after {
cursor: pointer;
display: inline-block;
position: absolute;
width: 16px;
height: 16px;
left: 0;
top: 0;
margin-left: -20px;
padding-left: 3px;
padding-top: 1px;
font-size: 11px;
color: #55595c; }
.abc-checkbox input[type="checkbox"],
.abc-checkbox input[type="radio"] {
cursor: pointer;
opacity: 0;
z-index: 1; }
.abc-checkbox input[type="checkbox"]:focus + label::before,
.abc-checkbox input[type="radio"]:focus + label::before {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px; }
.abc-checkbox input[type="checkbox"]:checked + label::after,
.abc-checkbox input[type="radio"]:checked + label::after {
font-family: "FontAwesome";
content: ""; }
.abc-checkbox input[type="checkbox"]:indeterminate + label::after,
.abc-checkbox input[type="radio"]:indeterminate + label::after {
display: block;
content: "";
width: 10px;
height: 3px;
background-color: #555555;
border-radius: 2px;
margin-left: -16.5px;
margin-top: 7px; }
.abc-checkbox input[type="checkbox"]:disabled + label,
.abc-checkbox input[type="radio"]:disabled + label {
opacity: 0.65; }
.abc-checkbox input[type="checkbox"]:disabled + label::before,
.abc-checkbox input[type="radio"]:disabled + label::before {
background-color: #eceeef;
cursor: not-allowed; }
.abc-checkbox.abc-checkbox-circle label::before {
border-radius: 50%; }
.abc-checkbox.checkbox-inline {
margin-top: 0; }
.abc-checkbox-primary input[type="checkbox"]:checked + label::before,
.abc-checkbox-primary input[type="radio"]:checked + label::before {
background-color: #0275d8;
border-color: #0275d8; }
.abc-checkbox-primary input[type="checkbox"]:checked + label::after,
.abc-checkbox-primary input[type="radio"]:checked + label::after {
color: #fff; }
.abc-checkbox-danger input[type="checkbox"]:checked + label::before,
.abc-checkbox-danger input[type="radio"]:checked + label::before {
background-color: #d9534f;
border-color: #d9534f; }
.abc-checkbox-danger input[type="checkbox"]:checked + label::after,
.abc-checkbox-danger input[type="radio"]:checked + label::after {
color: #fff; }
.abc-checkbox-info input[type="checkbox"]:checked + label::before,
.abc-checkbox-info input[type="radio"]:checked + label::before {
background-color: #5bc0de;
border-color: #5bc0de; }
.abc-checkbox-info input[type="checkbox"]:checked + label::after,
.abc-checkbox-info input[type="radio"]:checked + label::after {
color: #fff; }
.abc-checkbox-warning input[type="checkbox"]:checked + label::before,
.abc-checkbox-warning input[type="radio"]:checked + label::before {
background-color: #f0ad4e;
border-color: #f0ad4e; }
.abc-checkbox-warning input[type="checkbox"]:checked + label::after,
.abc-checkbox-warning input[type="radio"]:checked + label::after {
color: #fff; }
.abc-checkbox-success input[type="checkbox"]:checked + label::before,
.abc-checkbox-success input[type="radio"]:checked + label::before {
background-color: #5cb85c;
border-color: #5cb85c; }
.abc-checkbox-success input[type="checkbox"]:checked + label::after,
.abc-checkbox-success input[type="radio"]:checked + label::after {
color: #fff; }
.abc-checkbox-primary input[type="checkbox"]:indeterminate + label::before,
.abc-checkbox-primary input[type="radio"]:indeterminate + label::before {
background-color: #0275d8;
border-color: #0275d8; }
.abc-checkbox-primary input[type="checkbox"]:indeterminate + label::after,
.abc-checkbox-primary input[type="radio"]:indeterminate + label::after {
background-color: #fff; }
.abc-checkbox-danger input[type="checkbox"]:indeterminate + label::before,
.abc-checkbox-danger input[type="radio"]:indeterminate + label::before {
background-color: #d9534f;
border-color: #d9534f; }
.abc-checkbox-danger input[type="checkbox"]:indeterminate + label::after,
.abc-checkbox-danger input[type="radio"]:indeterminate + label::after {
background-color: #fff; }
.abc-checkbox-info input[type="checkbox"]:indeterminate + label::before,
.abc-checkbox-info input[type="radio"]:indeterminate + label::before {
background-color: #5bc0de;
border-color: #5bc0de; }
.abc-checkbox-info input[type="checkbox"]:indeterminate + label::after,
.abc-checkbox-info input[type="radio"]:indeterminate + label::after {
background-color: #fff; }
.abc-checkbox-warning input[type="checkbox"]:indeterminate + label::before,
.abc-checkbox-warning input[type="radio"]:indeterminate + label::before {
background-color: #f0ad4e;
border-color: #f0ad4e; }
.abc-checkbox-warning input[type="checkbox"]:indeterminate + label::after,
.abc-checkbox-warning input[type="radio"]:indeterminate + label::after {
background-color: #fff; }
.abc-checkbox-success input[type="checkbox"]:indeterminate + label::before,
.abc-checkbox-success input[type="radio"]:indeterminate + label::before {
background-color: #5cb85c;
border-color: #5cb85c; }
.abc-checkbox-success input[type="checkbox"]:indeterminate + label::after,
.abc-checkbox-success input[type="radio"]:indeterminate + label::after {
background-color: #fff; }
.abc-radio {
padding-left: 20px; }
.abc-radio label {
display: inline-block;
vertical-align: middle;
position: relative;
padding-left: 5px; }
.abc-radio label::before {
content: "";
cursor: pointer;
display: inline-block;
position: absolute;
width: 17px;
height: 17px;
left: 0;
margin-left: -20px;
border: 1px solid #ccc;
border-radius: 50%;
background-color: #fff; }
.abc-radio label::after {
cursor: pointer;
display: inline-block;
position: absolute;
content: " ";
width: 11px;
height: 11px;
left: 3px;
top: 3px;
margin-left: -20px;
border-radius: 50%;
background-color: #55595c;
transform: scale(0, 0);
transition: transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); }
.abc-radio input[type="radio"] {
cursor: pointer;
opacity: 0;
z-index: 1; }
.abc-radio input[type="radio"]:focus + label::before {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px; }
.abc-radio input[type="radio"]:checked + label::after {
transform: scale(1, 1); }
.abc-radio input[type="radio"]:disabled + label {
opacity: 0.65; }
.abc-radio input[type="radio"]:disabled + label::before {
cursor: not-allowed; }
.abc-radio.radio-inline {
margin-top: 0; }
.abc-radio-primary input[type="radio"] + label::after {
background-color: #0275d8; }
.abc-radio-primary input[type="radio"]:checked + label::before {
border-color: #0275d8; }
.abc-radio-primary input[type="radio"]:checked + label::after {
background-color: #0275d8; }
.abc-radio-danger input[type="radio"] + label::after {
background-color: #d9534f; }
.abc-radio-danger input[type="radio"]:checked + label::before {
border-color: #d9534f; }
.abc-radio-danger input[type="radio"]:checked + label::after {
background-color: #d9534f; }
.abc-radio-info input[type="radio"] + label::after {
background-color: #5bc0de; }
.abc-radio-info input[type="radio"]:checked + label::before {
border-color: #5bc0de; }
.abc-radio-info input[type="radio"]:checked + label::after {
background-color: #5bc0de; }
.abc-radio-warning input[type="radio"] + label::after {
background-color: #f0ad4e; }
.abc-radio-warning input[type="radio"]:checked + label::before {
border-color: #f0ad4e; }
.abc-radio-warning input[type="radio"]:checked + label::after {
background-color: #f0ad4e; }
.abc-radio-success input[type="radio"] + label::after {
background-color: #5cb85c; }
.abc-radio-success input[type="radio"]:checked + label::before {
border-color: #5cb85c; }
.abc-radio-success input[type="radio"]:checked + label::after {
background-color: #5cb85c; }
input[type="checkbox"].styled:checked + label:after,
input[type="radio"].styled:checked + label:after {
font-family: "FontAwesome";
content: ""; }
input[type="checkbox"] .styled:checked + label::before,
input[type="radio"] .styled:checked + label::before {
color: #fff; }
input[type="checkbox"] .styled:checked + label::after,
input[type="radio"] .styled:checked + label::after {
color: #fff; }

@ -0,0 +1,250 @@
//
// Checkboxes
// --------------------------------------------------
$font-family-icon: 'FontAwesome' !default;
$fa-var-check: "\f00c" !default;
$check-icon: $fa-var-check !default;
@mixin checkbox-variant($parent, $color) {
#{$parent} input[type="checkbox"]:checked + label,
#{$parent} input[type="radio"]:checked + label {
&::before {
background-color: $color;
border-color: $color;
}
&::after{
color: #fff;
}
}
}
@mixin checkbox-variant-indeterminate($parent, $color) {
#{$parent} input[type="checkbox"]:indeterminate + label,
#{$parent} input[type="radio"]:indeterminate + label {
&::before {
background-color: $color;
border-color: $color;
}
&::after{
background-color: #fff;
}
}
}
.abc-checkbox{
padding-left: 20px;
label{
display: inline-block;
vertical-align: middle;
position: relative;
padding-left: 5px;
&::before{
cursor: pointer;
content: "";
display: inline-block;
position: absolute;
width: 17px;
height: 17px;
left: 0;
margin-left: -20px;
border: 1px solid $input-border-color;
border-radius: 3px;
background-color: #fff;
@include transition(border 0.15s ease-in-out, color 0.15s ease-in-out);
}
&::after{
cursor: pointer;
display: inline-block;
position: absolute;
width: 16px;
height: 16px;
left: 0;
top: 0;
margin-left: -20px;
padding-left: 3px;
padding-top: 1px;
font-size: 11px;
color: $input-color;
}
}
input[type="checkbox"],
input[type="radio"] {
cursor: pointer;
opacity: 0;
z-index: 1;
&:focus + label::before{
@include tab-focus();
}
&:checked + label::after{
font-family: $font-family-icon;
content: $check-icon;
}
&:indeterminate + label::after{
display: block;
content: "";
width: 10px;
height: 3px;
background-color: #555555;
border-radius: 2px;
margin-left: -16.5px;
margin-top: 7px;
}
&:disabled + label{
opacity: 0.65;
&::before{
background-color: $input-bg-disabled;
cursor: not-allowed;
}
}
}
&.abc-checkbox-circle label::before{
border-radius: 50%;
}
&.checkbox-inline{
margin-top: 0;
}
}
@include checkbox-variant('.abc-checkbox-primary', $brand-primary);
@include checkbox-variant('.abc-checkbox-danger', $brand-danger);
@include checkbox-variant('.abc-checkbox-info', $brand-info);
@include checkbox-variant('.abc-checkbox-warning', $brand-warning);
@include checkbox-variant('.abc-checkbox-success', $brand-success);
@include checkbox-variant-indeterminate('.abc-checkbox-primary', $brand-primary);
@include checkbox-variant-indeterminate('.abc-checkbox-danger', $brand-danger);
@include checkbox-variant-indeterminate('.abc-checkbox-info', $brand-info);
@include checkbox-variant-indeterminate('.abc-checkbox-warning', $brand-warning);
@include checkbox-variant-indeterminate('.abc-checkbox-success', $brand-success);
//
// Radios
// --------------------------------------------------
@mixin radio-variant($parent, $color) {
#{$parent} input[type="radio"]{
+ label{
&::after{
background-color: $color;
}
}
&:checked + label{
&::before {
border-color: $color;
}
&::after{
background-color: $color;
}
}
}
}
.abc-radio{
padding-left: 20px;
label{
display: inline-block;
vertical-align: middle;
position: relative;
padding-left: 5px;
&::before{
cursor: pointer;
content: "";
display: inline-block;
position: absolute;
width: 17px;
height: 17px;
left: 0;
margin-left: -20px;
border: 1px solid $input-border-color;
border-radius: 50%;
background-color: #fff;
@include transition(border 0.15s ease-in-out);
}
&::after{
cursor: pointer;
display: inline-block;
position: absolute;
content: " ";
width: 11px;
height: 11px;
left: 3px;
top: 3px;
margin-left: -20px;
border-radius: 50%;
background-color: $input-color;
transform: scale(0, 0);
transition: transform .1s cubic-bezier(.8,-0.33,.2,1.33);
//curve - http://cubic-bezier.com/#.8,-0.33,.2,1.33
}
}
input[type="radio"]{
cursor: pointer;
opacity: 0;
z-index: 1;
&:focus + label::before{
@include tab-focus();
}
&:checked + label::after{
transform: scale(1, 1);
}
&:disabled + label{
opacity: 0.65;
&::before{
cursor: not-allowed;
}
}
}
&.radio-inline{
margin-top: 0;
}
}
@include radio-variant('.abc-radio-primary', $brand-primary);
@include radio-variant('.abc-radio-danger', $brand-danger);
@include radio-variant('.abc-radio-info', $brand-info);
@include radio-variant('.abc-radio-warning', $brand-warning);
@include radio-variant('.abc-radio-success', $brand-success);
input[type="checkbox"],
input[type="radio"] {
&.styled:checked + label:after {
font-family: $font-family-icon;
content: $check-icon;
}
.styled:checked + label {
&::before {
color: #fff;
}
&::after {
color: #fff;
}
}
}

@ -1 +0,0 @@
@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:13px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}

@ -540,6 +540,7 @@ function buildRequestContext(result, type) {
requestedUsers: result.requestedUsers ? result.requestedUsers.join(', ') : '',
requestedDate: Humanize(result.requestedDate),
requestedDateTicks: result.requestedDateTicks,
released: result.released,
available: result.available,
admin: result.admin,
issues: result.issues,
@ -551,7 +552,8 @@ function buildRequestContext(result, type) {
coverArtUrl: result.coverArtUrl,
qualities: result.qualities,
hasQualities: result.qualities && result.qualities.length > 0,
artist: result.artistName
artist: result.artistName,
musicBrainzId : result.musicBrainzId
};
return context;

@ -10,9 +10,11 @@
$(function () {
var searchSource = $("#search-template").html();
var seasonsSource = $("#seasons-template").html();
var musicSource = $("#music-template").html();
var searchTemplate = Handlebars.compile(searchSource);
var musicTemplate = Handlebars.compile(musicSource);
var seasonsTemplate = Handlebars.compile(seasonsSource);
var base = $('#baseUrl').text();
@ -29,6 +31,21 @@ $(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) {
@ -78,7 +95,9 @@ $(function () {
if (seasons === "1") {
// Send over the first season
data = data + "&seasons=first";
} if (seasons === "0") {
// Send over the first season
data = data + "&seasons=all";
}
var type = $form.prop('method');
@ -139,6 +158,32 @@ $(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");
}
});
});
function focusSearch($content) {
if ($content.length > 0) {
$('input[type=text].form-control', $content).first().focus();
@ -261,7 +306,7 @@ $(function () {
var html = musicTemplate(context);
$("#musicList").append(html);
getCoverArt(context.id);
getCoverArt(context.id);
});
}
else {
@ -345,4 +390,73 @@ $(function () {
return context;
}
$('#seasonsModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var id = button.data('identifier'); // Extract info from data-* attributes
var url = createBaseUrl(base, '/search/seasons/');
$.ajax({
type: "get",
url: url,
data: { tvId: id },
dataType: "json",
success: function (results) {
var $content = $("#seasonsBody");
$content.html("");
$('#selectedSeasonsId').val(id);
results.forEach(function(result) {
var context = buildSeasonsContext(result);
$content.append(seasonsTemplate(context));
});
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
function buildSeasonsContext(result) {
var context = {
id: result
};
return context;
};
});
$('#seasonsRequest').click(function(e) {
e.preventDefault();
var tvId = $('#selectedSeasonsId').val();
var url = createBaseUrl(base, '/search/seasons/');
if ($("#" + tvId).attr('disabled')) {
return;
}
$("#" + tvId).prop("disabled", true);
loadingButton(tvId, "primary");
var $form = $('#form' + tvId);
var data = $form.serialize();
var seasonsParam = "&seasons=";
var $checkedSeasons = $('.selectedSeasons:checkbox:checked');
$checkedSeasons.each(function (index, element) {
if (index < $checkedSeasons.length -1) {
seasonsParam = seasonsParam + element.id + ",";
} else {
seasonsParam = seasonsParam + element.id;
}
});
data = data + seasonsParam;
var type = $form.prop('method');
var url = $form.prop('action');
sendRequestAjax(data, type, url, tvId);
});
});

File diff suppressed because one or more lines are too long

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

Loading…
Cancel
Save