Merge pull request #231 from tidusjar/dev

Release 1.7
This commit is contained in:
Jamie 2016-05-24 19:31:45 +01:00
commit 6249a503f3
204 changed files with 55606 additions and 10615 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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));
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 Api.Execute<CouchPotatoMovies>(request, baseUrl);
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();
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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 };
var policy = RetryHandler.RetryAndWaitPolicy(
null,
(exception, timespan) => Log.Error(exception, "Exception when calling Ping for SR, Retrying {0}", timespan));
request.AddUrlSegment("apiKey", apiKey);
var obj = Api.ExecuteJson<SickRagePing>(request, baseUrl);
var obj = policy.Execute(() => Api.ExecuteJson<SickRagePing>(request, baseUrl));
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());
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();
}
}
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(() =>
return await Task.Run(
() =>
{
Log.Trace("Entering `Execute<SickRageTvAdd>` in a new `Task<T>`");
var result = Api.Execute<SickRageTvAdd>(request, baseUrl);
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);
}
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);
}
}
}

View file

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

View file

@ -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);
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 Api.Execute<List<Series>>(request, baseUrl);
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;
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,30 +34,6 @@
<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">
<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">
<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">
<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="Octokit, Version=0.19.0.0, Culture=neutral, processorArchitecture=MSIL">
<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">
<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" />
@ -69,6 +45,24 @@
<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>
</Reference>
<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>
</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="Octokit, Version=0.19.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\Octokit.0.19.0\lib\net45\Octokit.dll</HintPath>
</Reference>
<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>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="CacheKeys.cs" />
@ -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">

View file

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

View file

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

View file

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

View file

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

View file

@ -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
public void MigrateToVersion1700()
{
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
{
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");
}
}
}

View file

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

View file

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

View file

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

View file

@ -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)
{
Schedule<MediaCacheService>().ToRunNow();
}
public ApiRequestException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View file

@ -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,8 +86,12 @@ namespace PlexRequests.Helpers
public void Set(string key, object data, int cacheTime = 20)
{
var policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(cacheTime) };
lock (key)
{
Cache.Remove(key);
Cache.Add(new CacheItem(key, data), policy);
}
}
/// <summary>
/// Removes the specified object from the cache.
@ -97,9 +101,12 @@ namespace PlexRequests.Helpers
{
var keys = Cache.Where(x => x.Key.Contains(key));
foreach (var k in keys)
{
lock (key)
{
Cache.Remove(k.Key);
}
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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");
try
{
var movies = CpApi.GetMovies(settings.FullUri, settings.ApiKey, new[] { "active" });
Cache.Set(CacheKeys.CouchPotatoQueued, movies, 10);
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();
}
}
}

View file

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

View file

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

View file

@ -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;
namespace PlexRequests.Services
using Quartz;
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!
}
try
{
if (setCache)
{
Log.Trace("Plex Lib API Call");
results = GetLibraries(authSettings, plexSettings);
Cache.Set(CacheKeys.PlexLibaries, results, 10);
Log.Trace("Plex Lib Cache Set Call");
if (results != null)
{
Cache.Set(CacheKeys.PlexLibaries, results, CacheKeys.TimeFrameMinutes.SchedulerCaching);
}
}
else
{
results = Cache.GetOrSet(CacheKeys.PlexLibaries, () => {
Log.Trace("Plex Lib GetSet Call");
results = Cache.GetOrSet(CacheKeys.PlexLibaries, () =>
{
Log.Trace("Plex Lib API Call (inside getset)");
return GetLibraries(authSettings, plexSettings);
}, 10);
}, CacheKeys.TimeFrameMinutes.SchedulerCaching);
}
}
catch (Exception ex)
{
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();
}
}
}

View file

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

View file

@ -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;
namespace PlexRequests.Services
using Quartz;
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");
try
{
var series = SonarrApi.GetSeries(settings.ApiKey, settings.FullUri);
Cache.Set(CacheKeys.SonarrQueued, series, 10);
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();
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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">
<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>
<Reference Include="MailKit, Version=1.2.0.0, Culture=neutral, PublicKeyToken=4e064fe7c44a8f1b, processorArchitecture=MSIL">
<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">
<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>
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</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.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>
<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.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>
</Reference>
<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">
<HintPath>..\packages\MailKit.1.2.21\lib\net451\MailKit.dll</HintPath>
</Reference>
<Reference Include="MimeKit, Version=1.2.0.0, Culture=neutral, PublicKeyToken=bede1c8a46c66814">
<HintPath>..\packages\MimeKit.1.2.22\lib\net45\MimeKit.dll</HintPath>
</Reference>
<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">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
</Reference>
<Reference Include="Quartz, Version=2.3.3.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4">
<HintPath>..\packages\Quartz.2.3.3\lib\net40\Quartz.dll</HintPath>
</Reference>
</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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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