Merge pull request #332 from tidusjar/dev

1.8.0
pull/333/head^2
Jamie 9 years ago committed by GitHub
commit 2963f3843e

@ -40,5 +40,6 @@ namespace PlexRequests.Api.Interfaces
PlexAccount GetAccount(string authToken);
PlexLibraries GetLibrarySections(string authToken, Uri plexFullHost);
PlexSearch GetLibrary(string authToken, Uri plexFullHost, string libraryId);
PlexMetadata GetMetadata(string authToken, Uri plexFullHost, string itemId);
}
}

@ -64,6 +64,7 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

@ -0,0 +1,11 @@
<?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" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -27,11 +27,15 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace PlexRequests.Api.Models.Movie
{
public class ProfileList
{
public bool core { get; set; }
public bool hide { get; set; }
public string _rev { get; set; }
public List<bool> finish { get; set; }
public List<string> qualities { get; set; }
@ -40,9 +44,10 @@ namespace PlexRequests.Api.Models.Movie
public string label { get; set; }
public int minimum_score { get; set; }
public List<int> stop_after { get; set; }
public List<int> wait_for { get; set; }
public List<object> wait_for { get; set; }
public int order { get; set; }
public List<object> __invalid_name__3d { get; set; }
[JsonProperty(PropertyName = "3d")]
public List<object> threeD { get; set; }
}
public class CouchPotatoProfiles

@ -0,0 +1,58 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexMetadata.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Xml.Serialization;
namespace PlexRequests.Api.Models.Plex
{
[XmlRoot(ElementName = "MediaContainer")]
public class PlexMetadata
{
[XmlElement(ElementName= "Video")]
public Video Video { get; set; }
[XmlElement(ElementName = "Directory")]
public Directory1 Directory { get; set; }
[XmlAttribute(AttributeName = "size")]
public string Size { get; set; }
[XmlAttribute(AttributeName = "allowSync")]
public string AllowSync { get; set; }
[XmlAttribute(AttributeName = "identifier")]
public string Identifier { get; set; }
[XmlAttribute(AttributeName = "librarySectionID")]
public string LibrarySectionID { get; set; }
[XmlAttribute(AttributeName = "librarySectionTitle")]
public string LibrarySectionTitle { get; set; }
[XmlAttribute(AttributeName = "librarySectionUUID")]
public string LibrarySectionUUID { get; set; }
[XmlAttribute(AttributeName = "mediaTagPrefix")]
public string MediaTagPrefix { get; set; }
[XmlAttribute(AttributeName = "mediaTagVersion")]
public string MediaTagVersion { get; set; }
}
}

@ -133,6 +133,9 @@ namespace PlexRequests.Api.Models.Plex
[XmlRoot(ElementName = "Video")]
public class Video
{
public string ProviderId { get; set; }
[XmlAttribute(AttributeName = "guid")]
public string Guid { get; set; }
[XmlElement(ElementName = "Media")]
public List<Media> Media { get; set; }
[XmlElement(ElementName = "Genre")]
@ -241,6 +244,9 @@ namespace PlexRequests.Api.Models.Plex
[XmlRoot(ElementName = "Directory")]
public class Directory1
{
public string ProviderId { get; set; }
[XmlAttribute(AttributeName = "guid")]
public string Guid { get; set; }
[XmlElement(ElementName = "Genre")]
public List<Genre> Genre { get; set; }
[XmlElement(ElementName = "Role")]
@ -311,6 +317,7 @@ namespace PlexRequests.Api.Models.Plex
[XmlRoot(ElementName = "MediaContainer")]
public class PlexSearch
{
[XmlElement(ElementName = "Directory")]
public List<Directory1> Directory { get; set; }
[XmlElement(ElementName = "Video")]

@ -64,6 +64,7 @@
<Compile Include="Plex\PlexError.cs" />
<Compile Include="Plex\PlexFriends.cs" />
<Compile Include="Plex\PlexLibraries.cs" />
<Compile Include="Plex\PlexMetadata.cs" />
<Compile Include="Plex\PlexSearch.cs" />
<Compile Include="Plex\PlexStatus.cs" />
<Compile Include="Plex\PlexMediaType.cs" />
@ -90,6 +91,7 @@
<Compile Include="Tv\TvShowImages.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,11 @@
<?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" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -62,7 +62,7 @@ namespace PlexRequests.Api
request.AddUrlSegment("imdbid", imdbid);
request.AddUrlSegment("title", title);
var obj = RetryHandler.Execute<JObject>(() => Api.ExecuteJson<JObject> (request, baseUrl),new TimeSpan[] {
var obj = RetryHandler.Execute(() => Api.ExecuteJson<JObject> (request, baseUrl),new[] {
TimeSpan.FromSeconds (2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)},
@ -143,8 +143,8 @@ namespace PlexRequests.Api
request.AddUrlSegment("status", string.Join(",", status));
try
{
var obj = RetryHandler.Execute<CouchPotatoMovies>(() => Api.Execute<CouchPotatoMovies> (request, baseUrl),
new TimeSpan[] {
var obj = RetryHandler.Execute(() => Api.Execute<CouchPotatoMovies> (request, baseUrl),
new[] {
TimeSpan.FromSeconds (5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)

@ -220,6 +220,36 @@ namespace PlexRequests.Api
}
}
public PlexMetadata GetMetadata(string authToken, Uri plexFullHost, string itemId)
{
var request = new RestRequest
{
Method = Method.GET,
Resource = "library/metadata/{itemId}"
};
request.AddUrlSegment("itemId", itemId);
AddHeaders(ref request, authToken);
try
{
var lib = RetryHandler.Execute(() => Api.ExecuteXml<PlexMetadata>(request, plexFullHost),
new[] {
TimeSpan.FromSeconds (5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
},
(exception, timespan) => Log.Error(exception, "Exception when calling GetMetadata for Plex, Retrying {0}", timespan));
return lib;
}
catch (Exception e)
{
Log.Error(e, "There has been a API Exception when attempting to get the Plex GetMetadata");
return new PlexMetadata();
}
}
private void AddHeaders(ref RestRequest request, string authToken)
{
request.AddHeader("X-Plex-Token", authToken);

@ -31,6 +31,10 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.3.4\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\net45\RestSharp.dll</HintPath>
<Private>True</Private>
@ -52,9 +56,6 @@
<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>
</Reference>

@ -3,7 +3,7 @@
<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="NLog" version="4.3.4" 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" />

@ -33,21 +33,21 @@ namespace PlexRequests.Core
public const int SchedulerCaching = 60;
}
public const string PlexLibaries = "PlexLibaries";
public const string PlexLibaries = nameof(PlexLibaries);
public const string TvDbToken = "TheTvDbApiToken";
public const string TvDbToken = nameof(TvDbToken);
public const string SonarrQualityProfiles = "SonarrQualityProfiles";
public const string SonarrQueued = "SonarrQueued";
public const string SonarrQualityProfiles = nameof(SonarrQualityProfiles);
public const string SonarrQueued = nameof(SonarrQueued);
public const string SickRageQualityProfiles = "SickRageQualityProfiles";
public const string SickRageQueued = "SickRageQueued";
public const string SickRageQualityProfiles = nameof(SickRageQualityProfiles);
public const string SickRageQueued = nameof(SickRageQueued);
public const string CouchPotatoQualityProfiles = "CouchPotatoQualityProfiles";
public const string CouchPotatoQueued = "CouchPotatoQueued";
public const string CouchPotatoQualityProfiles = nameof(CouchPotatoQualityProfiles);
public const string CouchPotatoQueued = nameof(CouchPotatoQueued);
public const string GetPlexRequestSettings = "GetPlexRequestSettings";
public const string GetPlexRequestSettings = nameof(GetPlexRequestSettings);
public const string LastestProductVersion = "LatestProductVersion";
public const string LastestProductVersion = nameof(LastestProductVersion);
}
}

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

@ -49,7 +49,9 @@ namespace PlexRequests.Core
Task<RequestedModel> GetAsync(int id);
IEnumerable<RequestedModel> GetAll();
Task<IEnumerable<RequestedModel>> GetAllAsync();
bool BatchUpdate(List<RequestedModel> model);
bool BatchDelete(List<RequestedModel> model);
bool BatchUpdate(IEnumerable<RequestedModel> model);
Task<bool> BatchUpdateAsync(IEnumerable<RequestedModel> model);
bool BatchDelete(IEnumerable<RequestedModel> model);
Task<bool> BatchDeleteAsync(IEnumerable<RequestedModel> model);
}
}

@ -0,0 +1,115 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: IssueJsonService.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using PlexRequests.Core.Models;
using PlexRequests.Helpers;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
namespace PlexRequests.Core
{
public class IssueJsonService : IIssueService
{
public IssueJsonService(IRepository<IssueBlobs> repo)
{
Repo = repo;
}
private IRepository<IssueBlobs> Repo { get; }
public async Task<int> AddIssueAsync(IssuesModel model)
{
var entity = new IssueBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), RequestId = model.RequestId };
var id = await Repo.InsertAsync(entity);
model.Id = id;
entity = new IssueBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), RequestId = model.RequestId, Id = id };
var result = await Repo.UpdateAsync(entity);
return result ? id : -1;
}
public async Task<bool> UpdateIssueAsync(IssuesModel model)
{
var entity = new IssueBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), RequestId = model.RequestId, Id = model.Id };
return await Repo.UpdateAsync(entity);
}
public async Task DeleteIssueAsync(int id)
{
var entity = await Repo.GetAsync(id);
await Repo.DeleteAsync(entity);
}
public async Task DeleteIssueAsync(IssuesModel model)
{
var entity = new IssueBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), RequestId = model.RequestId, Id = model.Id };
await Repo.DeleteAsync(entity);
}
/// <summary>
/// Gets the Issues, if there is no issue in the DB we return a <c>new <see cref="IssuesModel"/></c>.
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns></returns>
public async Task<IssuesModel> GetAsync(int id)
{
var blob = await Repo.GetAsync(id);
if (blob == null)
{
return new IssuesModel();
}
var model = ByteConverterHelper.ReturnObject<IssuesModel>(blob.Content);
return model;
}
/// <summary>
/// Gets all the Issues, if there is no issue in the DB we return a <c>new IEnumerable&lt;IssuesModel&gt;</c>.
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<IssuesModel>> GetAllAsync()
{
var blobs = await Repo.GetAllAsync();
if (blobs == null)
{
return new List<IssuesModel>();
}
return blobs.Select(b => Encoding.UTF8.GetString(b.Content))
.Select(JsonConvert.DeserializeObject<IssuesModel>)
.ToList();
}
}
}

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: JsonRequestService.cs
// File: JsonRequestModelRequestService.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -38,9 +38,9 @@ using PlexRequests.Store.Repository;
namespace PlexRequests.Core
{
public class JsonRequestService : IRequestService
public class JsonRequestModelRequestService : IRequestService
{
public JsonRequestService(IRequestRepository repo)
public JsonRequestModelRequestService(IRequestRepository repo)
{
Repo = repo;
}
@ -161,16 +161,26 @@ namespace PlexRequests.Core
.ToList();
}
public bool BatchUpdate(List<RequestedModel> model)
public bool BatchUpdate(IEnumerable<RequestedModel> model)
{
var entities = model.Select(m => new RequestBlobs { Type = m.Type, Content = ByteConverterHelper.ReturnBytes(m), ProviderId = m.ProviderId, Id = m.Id }).ToList();
return Repo.UpdateAll(entities);
}
public bool BatchDelete(List<RequestedModel> model)
public async Task<bool> BatchUpdateAsync(IEnumerable<RequestedModel> model)
{
var entities = model.Select(m => new RequestBlobs { Type = m.Type, Content = ByteConverterHelper.ReturnBytes(m), ProviderId = m.ProviderId, Id = m.Id }).ToList();
return await Repo.UpdateAllAsync(entities);
}
public bool BatchDelete(IEnumerable<RequestedModel> model)
{
var entities = model.Select(m => new RequestBlobs { Type = m.Type, Content = ByteConverterHelper.ReturnBytes(m), ProviderId = m.ProviderId, Id = m.Id }).ToList();
return Repo.DeleteAll(entities);
}
public async Task<bool> BatchDeleteAsync(IEnumerable<RequestedModel> model)
{
var entities = model.Select(m => new RequestBlobs { Type = m.Type, Content = ByteConverterHelper.ReturnBytes(m), ProviderId = m.ProviderId, Id = m.Id }).ToList();
return await Repo.DeleteAllAsync(entities);
}
}
}

@ -0,0 +1,62 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: IssuesModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using PlexRequests.Store;
namespace PlexRequests.Core.Models
{
public class IssuesModel : Entity
{
public IssuesModel()
{
Issues = new List<IssueModel>();
}
public string Title { get; set; }
public string PosterUrl { get; set; }
public int RequestId { get; set; }
public List<IssueModel> Issues { get; set; }
public bool Deleted { get; set; }
public RequestType Type { get; set; }
public IssueStatus IssueStatus { get; set; }
public int ProviderId { get; set; }
}
public class IssueModel
{
public string UserNote { get; set; }
public string UserReported { get; set; }
public IssueState Issue { get; set; }
public string AdminNote { get; set; }
}
public enum IssueStatus
{
PendingIssue,
ResolvedIssue
}
}

@ -34,6 +34,10 @@
<Reference Include="Mono.Data.Sqlite">
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.3.4\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
@ -54,9 +58,6 @@
<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>
@ -66,13 +67,18 @@
</ItemGroup>
<ItemGroup>
<Compile Include="CacheKeys.cs" />
<Compile Include="IIssueService.cs" />
<Compile Include="IRequestService.cs" />
<Compile Include="ISettingsService.cs" />
<Compile Include="JsonRequestService.cs" />
<Compile Include="JsonIssuesModelRequestService.cs" />
<Compile Include="JsonRequestModelRequestService.cs" />
<Compile Include="Models\IssuesModel.cs" />
<Compile Include="Models\StatusModel.cs" />
<Compile Include="Models\UserProperties.cs" />
<Compile Include="SettingModels\AuthenticationSettings.cs" />
<Compile Include="SettingModels\HeadphonesSettings.cs" />
<Compile Include="SettingModels\LandingPageSettings.cs" />
<Compile Include="SettingModels\ScheduledJobsSettings.cs" />
<Compile Include="SettingModels\SlackNotificationSettings.cs" />
<Compile Include="SettingModels\PushoverNotificationSettings.cs" />
<Compile Include="SettingModels\PushBulletNotificationSettings.cs" />

@ -0,0 +1,46 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: LandingPageSettings.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 Newtonsoft.Json;
namespace PlexRequests.Core.SettingModels
{
public class LandingPageSettings : Settings
{
public bool Enabled { get; set; }
public bool BeforeLogin { get; set; }
public bool NoticeEnable { get; set; }
public string NoticeMessage { get; set; }
public bool EnabledNoticeTime { get; set; }
public DateTime NoticeStart { get; set; }
public DateTime NoticeEnd { get; set; }
[JsonIgnore]
public bool NoticeActive => DateTime.Now < NoticeEnd && DateTime.Now > NoticeStart;
}
}

@ -41,8 +41,10 @@ namespace PlexRequests.Core.SettingModels
public bool RequireTvShowApproval { get; set; }
public bool RequireMusicApproval { get; set; }
public bool UsersCanViewOnlyOwnRequests { get; set; }
public bool UsersCanViewOnlyOwnIssues { get; set; }
public int WeeklyRequestLimit { get; set; }
public string NoApprovalUsers { get; set; }
public bool CollectAnalyticData { get; set; }
/// <summary>
/// The CSS name of the theme we want

@ -37,6 +37,7 @@ namespace PlexRequests.Core.SettingModels
public int Port { get; set; }
public bool Ssl { get; set; }
public string SubDir { get; set; }
public bool AdvancedSearch { get; set; }
[JsonIgnore]
public Uri FullUri

@ -0,0 +1,47 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: ScheduledJobsSettings.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 ScheduledJobsSettings : Settings
{
public ScheduledJobsSettings()
{
PlexAvailabilityChecker = 10;
SickRageCacher = 10;
SonarrCacher = 10;
CouchPotatoCacher = 10;
StoreBackup = 24;
StoreCleanup = 24;
}
public int PlexAvailabilityChecker { get; set; }
public int SickRageCacher { get; set; }
public int SonarrCacher { get; set; }
public int CouchPotatoCacher { get; set; }
public int StoreBackup { get; set; }
public int StoreCleanup { get; set; }
}
}

@ -72,11 +72,7 @@ namespace PlexRequests.Core
return new T();
}
result.Content = DecryptSettings(result);
var obj = string.IsNullOrEmpty(result.Content) ? null : JsonConvert.DeserializeObject<T>(result.Content, SerializerSettings.Settings);
var model = obj;
return model;
return string.IsNullOrEmpty(result.Content) ? null : JsonConvert.DeserializeObject<T>(result.Content, SerializerSettings.Settings);
}
public bool SaveSettings(T model)

@ -61,7 +61,7 @@ namespace PlexRequests.Core
{
MigrateToVersion1700();
}
if (version > 1800 && version <= 1899)
if (version > 1799 && version <= 1800)
{
MigrateToVersion1800();
}
@ -181,10 +181,13 @@ namespace PlexRequests.Core
/// <summary>
/// Migrates to version 1.8.
/// <para>This includes updating the admin account to have all roles.</para>
/// <para>Set the log level to info</para>
/// <para>Set the log level to Error</para>
/// <para>Enable Analytics by default</para>
/// </summary>
private void MigrateToVersion1800()
{
// Give admin all roles/claims
try
{
var userMapper = new UserMapper(new UserRepository<UsersModel>(Db, new MemoryCacheProvider()));
@ -201,14 +204,15 @@ namespace PlexRequests.Core
catch (Exception e)
{
Log.Error(e);
throw;
}
// Set log level
try
{
var settingsService = new SettingsServiceV2<LogSettings>(new SettingsJsonRepository(Db, new MemoryCacheProvider()));
var logSettings = settingsService.GetSettings();
logSettings.Level = LogLevel.Info.Ordinal;
logSettings.Level = LogLevel.Error.Ordinal;
settingsService.SaveSettings(logSettings);
LoggingHelper.ReconfigureLogLevel(LogLevel.FromOrdinal(logSettings.Level));
@ -217,10 +221,24 @@ namespace PlexRequests.Core
catch (Exception e)
{
Log.Error(e);
throw;
}
// Enable analytics;
try
{
var prSettings = new SettingsServiceV2<PlexRequestSettings>(new SettingsJsonRepository(Db, new MemoryCacheProvider()));
var settings = prSettings.GetSettings();
settings.CollectAnalyticData = true;
var updated = prSettings.SaveSettings(settings);
}
catch (Exception e)
{
Log.Error(e);
}
}
}
}

@ -51,7 +51,7 @@ namespace PlexRequests.Core
return releases.FirstOrDefault();
}
public StatusModel GetStatus()
public async Task<StatusModel> GetStatus()
{
var assemblyVersion = AssemblyHelper.GetProductVersion();
var model = new StatusModel
@ -59,23 +59,23 @@ namespace PlexRequests.Core
Version = assemblyVersion,
};
var latestRelease = GetLatestRelease();
if (latestRelease.Result == null)
var latestRelease = await GetLatestRelease();
if (latestRelease == null)
{
return new StatusModel { Version = "Unknown" };
}
var latestVersionArray = latestRelease.Result.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries);
var latestVersionArray = latestRelease.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries);
var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty;
if (!latestVersion.Equals(assemblyVersion, StringComparison.InvariantCultureIgnoreCase))
{
model.UpdateAvailable = true;
model.UpdateUri = latestRelease.Result.HtmlUrl;
model.UpdateUri = latestRelease.HtmlUrl;
}
model.ReleaseNotes = latestRelease.Result.Body;
model.DownloadUri = latestRelease.Result.Assets[0].BrowserDownloadUrl;
model.ReleaseTitle = latestRelease.Result.Name;
model.ReleaseNotes = latestRelease.Body;
model.DownloadUri = latestRelease.Assets[0].BrowserDownloadUrl;
model.ReleaseTitle = latestRelease.Name;
return model;
}

@ -3,7 +3,7 @@
<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="NLog" version="4.3.4" targetFramework="net45" />
<package id="Octokit" version="0.19.0" targetFramework="net45" />
<package id="valueinjecter" version="3.1.1.2" targetFramework="net45" />
</packages>

@ -0,0 +1,54 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: DateTimeHelperTests.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 NUnit.Framework;
namespace PlexRequests.Helpers.Tests
{
[TestFixture]
public class DateTimeHelperTests
{
[TestCaseSource(nameof(OffsetUtcDateTimeData))]
public DateTime TestOffsetUtcDateTimeData(DateTime utcDateTime, int minuteOffset)
{
var offset = DateTimeHelper.OffsetUTCDateTime(utcDateTime, minuteOffset);
return offset.DateTime;
}
private static IEnumerable<TestCaseData> OffsetUtcDateTimeData
{
get
{
yield return new TestCaseData(new DateTime(2016,01,01,12,00,00), -60).Returns(new DateTimeOffset(new DateTime(2016, 01, 01, 13, 00, 00)).DateTime);
yield return new TestCaseData(new DateTime(2016,01,01,12,00,00), -120).Returns(new DateTimeOffset(new DateTime(2016, 01, 01, 14, 00, 00)).DateTime);
yield return new TestCaseData(new DateTime(2016,01,01,12,00,00), 120).Returns(new DateTimeOffset(new DateTime(2016, 01, 01, 10, 00, 00)).DateTime);
}
}
}
}

@ -0,0 +1,61 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UriHelperTests.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 NUnit.Framework;
namespace PlexRequests.Helpers.Tests
{
[TestFixture]
public class PlexHelperTests
{
[TestCaseSource(nameof(PlexGuids))]
public string CreateUriWithSubDir(string guid)
{
return PlexHelper.GetProviderIdFromPlexGuid(guid);
}
private static IEnumerable<TestCaseData> PlexGuids
{
get
{
yield return new TestCaseData("com.plexapp.agents.thetvdb://269586/3/17?lang=en").Returns("269586");
yield return new TestCaseData("com.plexapp.agents.imdb://tt3300542?lang=en").Returns("tt3300542");
yield return new TestCaseData("com.plexapp.agents.thetvdb://71326/10/5?lang=en").Returns("71326");
yield return new TestCaseData("local://3450").Returns("3450");
yield return new TestCaseData("com.plexapp.agents.imdb://tt1179933?lang=en").Returns("tt1179933");
yield return new TestCaseData("com.plexapp.agents.imdb://tt0284837?lang=en").Returns("tt0284837");
yield return new TestCaseData("com.plexapp.agents.imdb://tt0076759?lang=en").Returns("tt0076759");
}
}
}
}

@ -71,10 +71,12 @@
</Otherwise>
</Choose>
<ItemGroup>
<Compile Include="DateTimeHelperTests.cs" />
<Compile Include="PasswordHasherTests.cs" />
<Compile Include="HtmlRemoverTests.cs" />
<Compile Include="AssemblyHelperTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PlexHelperTests.cs" />
<Compile Include="UriHelperTests.cs" />
</ItemGroup>
<ItemGroup>

@ -1,15 +1,15 @@
<?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>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0"/>
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /></startup></configuration>

@ -0,0 +1,39 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: Action.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.Helpers.Analytics
{
public enum Action
{
Donate,
ClickButton,
Delete,
Create,
Save,
Update,
Start
}
}

@ -0,0 +1,240 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: Analytics.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.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using NLog;
using HttpUtility = Nancy.Helpers.HttpUtility;
namespace PlexRequests.Helpers.Analytics
{
public class Analytics : IAnalytics
{
private const string AnalyticsUri = "http://www.google-analytics.com/collect";
private const string RequestMethod = "POST";
private const string TrackingId = "UA-77083919-2";
private static Logger Log = LogManager.GetCurrentClassLogger();
public void TrackEvent(Category category, Action action, string label, string username, int? value = null)
{
var cat = category.ToString();
var act = action.ToString();
Track(HitType.@event, username, cat, act, label, value);
}
public async Task TrackEventAsync(Category category, Action action, string label, string username, int? value = null)
{
var cat = category.ToString();
var act = action.ToString();
await TrackAsync(HitType.@event, username, cat, act, label, value);
}
public void TrackPageview(Category category, Action action, string label, string username, int? value = null)
{
var cat = category.ToString();
var act = action.ToString();
Track(HitType.@pageview, username, cat, act, label, value);
}
public async Task TrackPageviewAsync(Category category, Action action, string label, string username, int? value = null)
{
var cat = category.ToString();
var act = action.ToString();
await TrackAsync(HitType.@pageview, username, cat, act, label, value);
}
public void TrackException(string message, string username, bool fatal)
{
var fatalInt = fatal ? 1 : 0;
Track(HitType.exception, message, fatalInt, username);
}
public async Task TrackExceptionAsync(string message, string username, bool fatal)
{
var fatalInt = fatal ? 1 : 0;
await TrackAsync(HitType.exception, message, fatalInt, username);
}
private void Track(HitType type, string username, string category, string action, string label, int? value = null)
{
if (string.IsNullOrEmpty(category)) throw new ArgumentNullException(nameof(category));
if (string.IsNullOrEmpty(action)) throw new ArgumentNullException(nameof(action));
if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username));
var postData = BuildRequestData(type, username, category, action, label, value, null, null);
var postDataString = postData
.Aggregate("", (data, next) => string.Format($"{data}&{next.Key}={HttpUtility.UrlEncode(next.Value)}"))
.TrimEnd('&');
SendRequest(postDataString);
}
private async Task TrackAsync(HitType type, string username, string category, string action, string label, int? value = null)
{
if (string.IsNullOrEmpty(category)) throw new ArgumentNullException(nameof(category));
if (string.IsNullOrEmpty(action)) throw new ArgumentNullException(nameof(action));
if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username));
var postData = BuildRequestData(type, username, category, action, label, value, null, null);
var postDataString = postData
.Aggregate("", (data, next) => string.Format($"{data}&{next.Key}={HttpUtility.UrlEncode(next.Value)}"))
.TrimEnd('&');
await SendRequestAsync(postDataString);
}
private async Task TrackAsync(HitType type, string message, int fatal, string username)
{
if (string.IsNullOrEmpty(message)) throw new ArgumentNullException(nameof(message));
if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username));
var postData = BuildRequestData(type, username, null, null, null, null, message, fatal);
var postDataString = postData
.Aggregate("", (data, next) => string.Format($"{data}&{next.Key}={HttpUtility.UrlEncode(next.Value)}"))
.TrimEnd('&');
await SendRequestAsync(postDataString);
}
private void Track(HitType type, string message, int fatal, string username)
{
if (string.IsNullOrEmpty(message)) throw new ArgumentNullException(nameof(message));
if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username));
var postData = BuildRequestData(type, username, null, null, null, null, message, fatal);
var postDataString = postData
.Aggregate("", (data, next) => string.Format($"{data}&{next.Key}={HttpUtility.UrlEncode(next.Value)}"))
.TrimEnd('&');
SendRequest(postDataString);
}
private void SendRequest(string postDataString)
{
var request = (HttpWebRequest)WebRequest.Create(AnalyticsUri);
request.Method = RequestMethod;
// set the Content-Length header to the correct value
request.ContentLength = Encoding.UTF8.GetByteCount(postDataString);
// write the request body to the request
using (var writer = new StreamWriter(request.GetRequestStream()))
{
writer.Write(postDataString);
}
try
{
var webResponse = (HttpWebResponse)request.GetResponse();
if (webResponse.StatusCode != HttpStatusCode.OK)
{
throw new HttpException((int)webResponse.StatusCode, "Google Analytics tracking did not return OK 200");
}
}
catch (Exception ex)
{
Log.Error(ex, "Analytics tracking failed");
}
}
private async Task SendRequestAsync(string postDataString)
{
var request = (HttpWebRequest)WebRequest.Create(AnalyticsUri);
request.Method = RequestMethod;
// set the Content-Length header to the correct value
request.ContentLength = Encoding.UTF8.GetByteCount(postDataString);
// write the request body to the request
using (var writer = new StreamWriter(await request.GetRequestStreamAsync()))
{
await writer.WriteAsync(postDataString);
}
try
{
var webResponse = (HttpWebResponse)await request.GetResponseAsync();
if (webResponse.StatusCode != HttpStatusCode.OK)
{
throw new HttpException((int)webResponse.StatusCode, "Google Analytics tracking did not return OK 200");
}
}
catch (Exception ex)
{
Log.Error(ex, "Analytics tracking failed");
}
}
private Dictionary<string, string> BuildRequestData(HitType type, string username, string category, string action, string label, int? value, string exceptionDescription, int? fatal)
{
var postData = new Dictionary<string, string>
{
{ "v", "1" },
{ "tid", TrackingId },
{ "t", type.ToString() },
{"cid", Guid.NewGuid().ToString() }
};
if (!string.IsNullOrEmpty(username))
{
postData.Add("uid", username);
}
if (!string.IsNullOrEmpty(label))
{
postData.Add("el", label);
}
if (value.HasValue)
{
postData.Add("ev", value.ToString());
}
if (!string.IsNullOrEmpty(category))
{
postData.Add("ec", category);
}
if (!string.IsNullOrEmpty(action))
{
postData.Add("ea", action);
}
if (!string.IsNullOrEmpty(exceptionDescription))
{
postData.Add("exd", exceptionDescription);
}
if (fatal.HasValue)
{
postData.Add("exf", fatal.ToString());
}
return postData;
}
}
}

@ -0,0 +1,41 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: Category.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.Helpers.Analytics
{
public enum Category
{
Startup,
Search,
Requests,
Admin,
LandingPage,
Api,
Issues,
UserLogin,
Services,
}
}

@ -0,0 +1,36 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: HitType.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
// ReSharper disable InconsistentNaming
namespace PlexRequests.Helpers.Analytics
{
internal enum HitType
{
@event,
@pageview,
@exception
}
}

@ -0,0 +1,92 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: IAnalytics.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Threading.Tasks;
namespace PlexRequests.Helpers.Analytics
{
public interface IAnalytics
{
/// <summary>
/// Tracks the event.
/// </summary>
/// <param name="category">The category.</param>
/// <param name="action">The action.</param>
/// <param name="label">The label.</param>
/// <param name="username">The username.</param>
/// <param name="value">The value.</param>
void TrackEvent(Category category, Action action, string label, string username, int? value = null);
/// <summary>
/// Tracks the event asynchronous.
/// </summary>
/// <param name="category">The category.</param>
/// <param name="action">The action.</param>
/// <param name="label">The label.</param>
/// <param name="username">The username.</param>
/// <param name="value">The value.</param>
/// <returns></returns>
Task TrackEventAsync(Category category, Action action, string label, string username, int? value = null);
/// <summary>
/// Tracks the page view.
/// </summary>
/// <param name="category">The category.</param>
/// <param name="action">The action.</param>
/// <param name="label">The label.</param>
/// <param name="username">The username.</param>
/// <param name="value">The value.</param>
void TrackPageview(Category category, Action action, string label, string username, int? value = null);
/// <summary>
/// Tracks the page view asynchronous.
/// </summary>
/// <param name="category">The category.</param>
/// <param name="action">The action.</param>
/// <param name="label">The label.</param>
/// <param name="username">The username.</param>
/// <param name="value">The value.</param>
/// <returns></returns>
Task TrackPageviewAsync(Category category, Action action, string label, string username, int? value = null);
/// <summary>
/// Tracks the exception.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="username">The username.</param>
/// <param name="fatal">if set to <c>true</c> [fatal].</param>
void TrackException(string message, string username, bool fatal);
/// <summary>
/// Tracks the exception asynchronous.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="username">The username.</param>
/// <param name="fatal">if set to <c>true</c> [fatal].</param>
/// <returns></returns>
Task TrackExceptionAsync(string message, string username, bool fatal);
}
}

@ -139,7 +139,6 @@ namespace PlexRequests.Helpers
if (level == LogLevel.Info)
{
rule.EnableLoggingForLevel(LogLevel.Info);
rule.EnableLoggingForLevel(LogLevel.Debug);
rule.EnableLoggingForLevel(LogLevel.Warn);
rule.EnableLoggingForLevel(LogLevel.Error);
rule.EnableLoggingForLevel(LogLevel.Fatal);

@ -0,0 +1,47 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexHelper.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;
namespace PlexRequests.Helpers
{
public class PlexHelper
{
public static string GetProviderIdFromPlexGuid(string guid)
{
if (string.IsNullOrEmpty(guid))
return guid;
var guidSplit = guid.Split(new[] {'/', '?'}, StringSplitOptions.RemoveEmptyEntries);
if (guidSplit.Length > 1)
{
return guidSplit[1];
}
return string.Empty;
}
}
}

@ -31,9 +31,26 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Hangfire.Core, Version=1.5.7.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Hangfire.Core.1.5.7\lib\net45\Hangfire.Core.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="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.3.4\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL">
<HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Caching" />
<Reference Include="System.Web" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
@ -43,11 +60,13 @@
<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="Analytics\Action.cs" />
<Compile Include="Analytics\Analytics.cs" />
<Compile Include="Analytics\Category.cs" />
<Compile Include="Analytics\HitType.cs" />
<Compile Include="Analytics\IAnalytics.cs" />
<Compile Include="AssemblyHelper.cs" />
<Compile Include="ByteConverterHelper.cs" />
<Compile Include="DateTimeHelper.cs" />
@ -60,6 +79,7 @@
<Compile Include="MemoryCacheProvider.cs" />
<Compile Include="ObjectCopier.cs" />
<Compile Include="PasswordHasher.cs" />
<Compile Include="PlexHelper.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SerializerSettings.cs" />
<Compile Include="StringCipher.cs" />
@ -67,6 +87,7 @@
<Compile Include="UserClaims.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup />

@ -0,0 +1,11 @@
<?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" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Hangfire.Core" version="1.5.7" 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="NLog" version="4.3.4" targetFramework="net45" />
<package id="Owin" version="1.0" targetFramework="net45" />
</packages>

@ -25,20 +25,19 @@
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
using PlexRequests.Helpers;
using PlexRequests.Services.Jobs;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
namespace PlexRequests.Services.Tests
{
@ -46,19 +45,40 @@ namespace PlexRequests.Services.Tests
public class PlexAvailabilityCheckerTests
{
public IAvailabilityChecker Checker { get; set; }
private Mock<ISettingsService<PlexSettings>> SettingsMock { get; set; }
private Mock<ISettingsService<AuthenticationSettings>> AuthMock { get; set; }
private Mock<IRequestService> RequestMock { get; set; }
private Mock<IPlexApi> PlexMock { get; set; }
private Mock<ICacheProvider> CacheMock { get; set; }
private Mock<INotificationService> NotificationMock { get; set; }
private Mock<IJobRecord> JobRec { get; set; }
private Mock<IRepository<UsersToNotify>> NotifyUsers { get; set; }
[SetUp]
public void Setup()
{
SettingsMock = new Mock<ISettingsService<PlexSettings>>();
AuthMock = new Mock<ISettingsService<AuthenticationSettings>>();
RequestMock = new Mock<IRequestService>();
PlexMock = new Mock<IPlexApi>();
NotificationMock = new Mock<INotificationService>();
CacheMock = new Mock<ICacheProvider>();
NotifyUsers = new Mock<IRepository<UsersToNotify>>();
JobRec = new Mock<IJobRecord>();
Checker = new PlexAvailabilityChecker(SettingsMock.Object, AuthMock.Object, RequestMock.Object, PlexMock.Object, CacheMock.Object, NotificationMock.Object, JobRec.Object, NotifyUsers.Object);
//[Test]
//public void IsAvailableWithEmptySettingsTest()
//{
// var settingsMock = new Mock<ISettingsService<PlexSettings>>();
// var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
// var requestMock = new Mock<IRequestService>();
// var plexMock = new Mock<IPlexApi>();
// var cacheMock = new Mock<ICacheProvider>();
// Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object, cacheMock.Object);
}
// Assert.Throws<ApplicationSettingsException>(() => Checker.IsAvailable("title", "2013", null, PlexType.TvShow), "We should be throwing an exception since we cannot talk to the services.");
//}
[Test]
public void InvalidSettings()
{
Checker.CheckAndUpdateAll();
PlexMock.Verify(x => x.GetLibrary(It.IsAny<string>(), It.IsAny<Uri>(), It.IsAny<string>()), Times.Never);
PlexMock.Verify(x => x.GetAccount(It.IsAny<string>()), Times.Never);
PlexMock.Verify(x => x.GetLibrarySections(It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
PlexMock.Verify(x => x.GetStatus(It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
PlexMock.Verify(x => x.GetUsers(It.IsAny<string>()), Times.Never);
}
//[Test]
//public void IsAvailableTest()
@ -531,7 +551,7 @@ namespace PlexRequests.Services.Tests
// {
// Title = "Hi",
// Year = "2017",
// ParentTitle = "c"
//ParentTitle = "c"
// },
// new Directory1
// {

@ -36,6 +36,14 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Common.Logging, Version=3.0.0.0, Culture=neutral, PublicKeyToken=af08829b84f0328e, processorArchitecture=MSIL">
<HintPath>..\packages\Common.Logging.3.0.0\lib\net40\Common.Logging.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Common.Logging.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=af08829b84f0328e, processorArchitecture=MSIL">
<HintPath>..\packages\Common.Logging.Core.3.0.0\lib\net40\Common.Logging.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<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>
@ -44,6 +52,10 @@
<HintPath>..\packages\NUnit.3.2.0\lib\net45\nunit.framework.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Quartz, Version=2.3.3.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4, processorArchitecture=MSIL">
<HintPath>..\packages\Quartz.2.3.3\lib\net40\Quartz.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
</ItemGroup>
<Choose>
@ -63,6 +75,9 @@
<None Include="app.config">
<SubType>Designer</SubType>
</None>
<None Include="job_scheduling_data_2_0.xsd">
<SubType>Designer</SubType>
</None>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,361 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://quartznet.sourceforge.net/JobSchedulingData"
targetNamespace="http://quartznet.sourceforge.net/JobSchedulingData"
elementFormDefault="qualified"
version="2.0">
<xs:element name="job-scheduling-data">
<xs:annotation>
<xs:documentation>Root level node</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence maxOccurs="unbounded">
<xs:element name="pre-processing-commands" type="pre-processing-commandsType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Commands to be executed before scheduling the jobs and triggers in this file.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="processing-directives" type="processing-directivesType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Directives to be followed while scheduling the jobs and triggers in this file.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="schedule" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence maxOccurs="unbounded">
<xs:element name="job" type="job-detailType" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="trigger" type="triggerType" minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="version" type="xs:string">
<xs:annotation>
<xs:documentation>Version of the XML Schema instance</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:complexType name="pre-processing-commandsType">
<xs:sequence maxOccurs="unbounded">
<xs:element name="delete-jobs-in-group" type="xs:string" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Delete all jobs, if any, in the identified group. "*" can be used to identify all groups. Will also result in deleting all triggers related to the jobs.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="delete-triggers-in-group" type="xs:string" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Delete all triggers, if any, in the identified group. "*" can be used to identify all groups. Will also result in deletion of related jobs that are non-durable.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="delete-job" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Delete the identified job if it exists (will also result in deleting all triggers related to it).</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="group" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="delete-trigger" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Delete the identified trigger if it exists (will also result in deletion of related jobs that are non-durable).</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="group" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="processing-directivesType">
<xs:sequence>
<xs:element name="overwrite-existing-data" type="xs:boolean" minOccurs="0" default="true">
<xs:annotation>
<xs:documentation>Whether the existing scheduling data (with same identifiers) will be overwritten. If false, and ignore-duplicates is not false, and jobs or triggers with the same names already exist as those in the file, an error will occur.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ignore-duplicates" type="xs:boolean" minOccurs="0" default="false">
<xs:annotation>
<xs:documentation>If true (and overwrite-existing-data is false) then any job/triggers encountered in this file that have names that already exist in the scheduler will be ignored, and no error will be produced.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="schedule-trigger-relative-to-replaced-trigger" type="xs:boolean" minOccurs="0" default="false">
<xs:annotation>
<xs:documentation>If true trigger's start time is calculated based on earlier run time instead of fixed value. Trigger's start time must be undefined for this to work.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="job-detailType">
<xs:annotation>
<xs:documentation>Define a JobDetail</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="group" type="xs:string" minOccurs="0" />
<xs:element name="description" type="xs:string" minOccurs="0" />
<xs:element name="job-type" type="xs:string" />
<xs:sequence minOccurs="0">
<xs:element name="durable" type="xs:boolean" />
<xs:element name="recover" type="xs:boolean" />
</xs:sequence>
<xs:element name="job-data-map" type="job-data-mapType" minOccurs="0" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="job-data-mapType">
<xs:annotation>
<xs:documentation>Define a JobDataMap</xs:documentation>
</xs:annotation>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="entry" type="entryType" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="entryType">
<xs:annotation>
<xs:documentation>Define a JobDataMap entry</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="key" type="xs:string" />
<xs:element name="value" type="xs:string" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="triggerType">
<xs:annotation>
<xs:documentation>Define a Trigger</xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element name="simple" type="simpleTriggerType" />
<xs:element name="cron" type="cronTriggerType" />
<xs:element name="calendar-interval" type="calendarIntervalTriggerType" />
</xs:choice>
</xs:complexType>
<xs:complexType name="abstractTriggerType" abstract="true">
<xs:annotation>
<xs:documentation>Common Trigger definitions</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="group" type="xs:string" minOccurs="0" />
<xs:element name="description" type="xs:string" minOccurs="0" />
<xs:element name="job-name" type="xs:string" />
<xs:element name="job-group" type="xs:string" minOccurs="0" />
<xs:element name="priority" type="xs:nonNegativeInteger" minOccurs="0" />
<xs:element name="calendar-name" type="xs:string" minOccurs="0" />
<xs:element name="job-data-map" type="job-data-mapType" minOccurs="0" />
<xs:sequence minOccurs="0">
<xs:choice>
<xs:element name="start-time" type="xs:dateTime" />
<xs:element name="start-time-seconds-in-future" type="xs:nonNegativeInteger" />
</xs:choice>
<xs:element name="end-time" type="xs:dateTime" minOccurs="0" />
</xs:sequence>
</xs:sequence>
</xs:complexType>
<xs:complexType name="simpleTriggerType">
<xs:annotation>
<xs:documentation>Define a SimpleTrigger</xs:documentation>
</xs:annotation>
<xs:complexContent>
<xs:extension base="abstractTriggerType">
<xs:sequence>
<xs:element name="misfire-instruction" type="simple-trigger-misfire-instructionType" minOccurs="0" />
<xs:sequence minOccurs="0">
<xs:element name="repeat-count" type="repeat-countType" />
<xs:element name="repeat-interval" type="xs:nonNegativeInteger" />
</xs:sequence>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="cronTriggerType">
<xs:annotation>
<xs:documentation>Define a CronTrigger</xs:documentation>
</xs:annotation>
<xs:complexContent>
<xs:extension base="abstractTriggerType">
<xs:sequence>
<xs:element name="misfire-instruction" type="cron-trigger-misfire-instructionType" minOccurs="0" />
<xs:element name="cron-expression" type="cron-expressionType" />
<xs:element name="time-zone" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="calendarIntervalTriggerType">
<xs:annotation>
<xs:documentation>Define a DateIntervalTrigger</xs:documentation>
</xs:annotation>
<xs:complexContent>
<xs:extension base="abstractTriggerType">
<xs:sequence>
<xs:element name="misfire-instruction" type="date-interval-trigger-misfire-instructionType" minOccurs="0" />
<xs:element name="repeat-interval" type="xs:nonNegativeInteger" />
<xs:element name="repeat-interval-unit" type="interval-unitType" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:simpleType name="cron-expressionType">
<xs:annotation>
<xs:documentation>
Cron expression (see JavaDoc for examples)
Special thanks to Chris Thatcher (thatcher@butterfly.net) for the regular expression!
Regular expressions are not my strong point but I believe this is complete,
with the caveat that order for expressions like 3-0 is not legal but will pass,
and month and day names must be capitalized.
If you want to examine the correctness look for the [\s] to denote the
seperation of individual regular expressions. This is how I break them up visually
to examine them:
SECONDS:
(
((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)
| (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))
| ([\?])
| ([\*])
) [\s]
MINUTES:
(
((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)
| (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))
| ([\?])
| ([\*])
) [\s]
HOURS:
(
((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?)
| (([\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3]))
| ([\?])
| ([\*])
) [\s]
DAY OF MONTH:
(
((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?)
| (([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?)
| (L(-[0-9])?)
| (L(-[1-2][0-9])?)
| (L(-[3][0-1])?)
| (LW)
| ([1-9]W)
| ([1-3][0-9]W)
| ([\?])
| ([\*])
)[\s]
MONTH:
(
((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?)
| (([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2]))
| (((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)
| ((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))
| ([\?])
| ([\*])
)[\s]
DAY OF WEEK:
(
(([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?)
| ([1-7]/([1-7]))
| (((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?)
| ((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?)
| (([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))(L|LW)?)
| (([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?)
| ([\?])
| ([\*])
)
YEAR (OPTIONAL):
(
[\s]?
([\*])?
| ((19[7-9][0-9])|(20[0-9][0-9]))?
| (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))?
| ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)?
)
</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern
value="(((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\?])|([\*]))[\s](((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\?])|([\*]))[\s](((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?)|(([\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3]))|([\?])|([\*]))[\s](((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?)|(([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?)|(L(-[0-9])?)|(L(-[1-2][0-9])?)|(L(-[3][0-1])?)|(LW)|([1-9]W)|([1-3][0-9]W)|([\?])|([\*]))[\s](((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?)|(([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2]))|(((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)|((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))|([\?])|([\*]))[\s]((([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?)|([1-7]/([1-7]))|(((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?)|((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?)|(([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))?(L|LW)?)|(([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?)|([\?])|([\*]))([\s]?(([\*])?|(19[7-9][0-9])|(20[0-9][0-9]))?| (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))?| ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)?)" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="repeat-countType">
<xs:annotation>
<xs:documentation>Number of times to repeat the Trigger (-1 for indefinite)</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:integer">
<xs:minInclusive value="-1" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="simple-trigger-misfire-instructionType">
<xs:annotation>
<xs:documentation>Simple Trigger Misfire Instructions</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="SmartPolicy" />
<xs:pattern value="RescheduleNextWithExistingCount" />
<xs:pattern value="RescheduleNextWithRemainingCount" />
<xs:pattern value="RescheduleNowWithExistingRepeatCount" />
<xs:pattern value="RescheduleNowWithRemainingRepeatCount" />
<xs:pattern value="FireNow" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="cron-trigger-misfire-instructionType">
<xs:annotation>
<xs:documentation>Cron Trigger Misfire Instructions</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="SmartPolicy" />
<xs:pattern value="DoNothing" />
<xs:pattern value="FireOnceNow" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="date-interval-trigger-misfire-instructionType">
<xs:annotation>
<xs:documentation>Date Interval Trigger Misfire Instructions</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="SmartPolicy" />
<xs:pattern value="DoNothing" />
<xs:pattern value="FireOnceNow" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="interval-unitType">
<xs:annotation>
<xs:documentation>Interval Units</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="Day" />
<xs:pattern value="Hour" />
<xs:pattern value="Minute" />
<xs:pattern value="Month" />
<xs:pattern value="Second" />
<xs:pattern value="Week" />
<xs:pattern value="Year" />
</xs:restriction>
</xs:simpleType>
</xs:schema>

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Common.Logging" version="3.0.0" targetFramework="net452" />
<package id="Common.Logging.Core" version="3.0.0" targetFramework="net452" />
<package id="Moq" version="4.2.1510.2205" targetFramework="net46" />
<package id="NUnit" version="3.2.0" targetFramework="net46" />
<package id="Quartz" version="2.3.3" targetFramework="net452" />
</packages>

@ -33,9 +33,9 @@ namespace PlexRequests.Services.Interfaces
{
void CheckAndUpdateAll();
List<PlexMovie> GetPlexMovies();
bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year);
bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year, string providerId = null);
List<PlexTvShow> GetPlexTvShows();
bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year);
bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year, string providerId = null);
List<PlexAlbum> GetPlexAlbums();
bool IsAlbumAvailable(PlexAlbum[] plexAlbums, string title, string year, string artist);
}

@ -24,10 +24,17 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Threading.Tasks;
using PlexRequests.Store.Models;
namespace PlexRequests.Services.Interfaces
{
public interface IJobRecord
{
void Record(string jobName);
Task<IEnumerable<ScheduledJobs>> GetJobsAsync();
IEnumerable<ScheduledJobs> GetJobs();
}
}

@ -25,13 +25,15 @@
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
namespace PlexRequests.Services
namespace PlexRequests.Services.Jobs
{
public class JobRecord : IJobRecord
{
@ -39,7 +41,9 @@ namespace PlexRequests.Services
{
Repo = repo;
}
private IRepository<ScheduledJobs> Repo { get; }
public void Record(string jobName)
{
var allJobs = Repo.GetAll();
@ -55,5 +59,15 @@ namespace PlexRequests.Services
Repo.Insert(job);
}
}
public async Task<IEnumerable<ScheduledJobs>> GetJobsAsync()
{
return await Repo.GetAllAsync();
}
public IEnumerable<ScheduledJobs> GetJobs()
{
return Repo.GetAll();
}
}
}

@ -35,6 +35,7 @@ using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Models;
using PlexRequests.Services.Notification;
@ -44,6 +45,8 @@ using PlexRequests.Store.Repository;
using Quartz;
using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.Services.Jobs
{
public class PlexAvailabilityChecker : IJob, IAvailabilityChecker
@ -72,14 +75,12 @@ namespace PlexRequests.Services.Jobs
private IRepository<UsersToNotify> UserNotifyRepo { get; }
public void CheckAndUpdateAll()
{
Log.Trace("Getting the settings");
var plexSettings = Plex.GetSettings();
var authSettings = Auth.GetSettings();
Log.Trace("Getting all the requests");
if (!ValidateSettings(plexSettings, authSettings))
{
Log.Info("Validation of the plex settings failed.");
Log.Debug("Validation of the plex settings failed.");
return;
}
@ -87,7 +88,7 @@ namespace PlexRequests.Services.Jobs
if (libraries == null || !libraries.Any())
{
Log.Info("Did not find any libraries in Plex.");
Log.Debug("Did not find any libraries in Plex.");
return;
}
@ -97,31 +98,26 @@ namespace PlexRequests.Services.Jobs
var requests = RequestService.GetAll();
var requestedModels = requests as RequestedModel[] ?? requests.Where(x => !x.Available).ToArray();
Log.Trace("Requests Count {0}", requestedModels.Length);
if (!requestedModels.Any())
{
Log.Info("There are no requests to check.");
Log.Debug("There are no requests to check.");
return;
}
var modifiedModel = new List<RequestedModel>();
foreach (var r in requestedModels)
{
Log.Trace("We are going to see if Plex has the following title: {0}", r.Title);
Log.Trace("Search results from Plex for the following request: {0}", r.Title);
var releaseDate = r.ReleaseDate == DateTime.MinValue ? string.Empty : r.ReleaseDate.ToString("yyyy");
bool matchResult;
switch (r.Type)
{
case RequestType.Movie:
matchResult = IsMovieAvailable(movies, r.Title, releaseDate);
matchResult = IsMovieAvailable(movies, r.Title, releaseDate, r.ImdbId);
break;
case RequestType.TvShow:
matchResult = IsTvShowAvailable(shows, r.Title, releaseDate);
matchResult = IsTvShowAvailable(shows, r.Title, releaseDate, r.TvDbId);
break;
case RequestType.Album:
matchResult = IsAlbumAvailable(albums, r.Title, r.ReleaseDate.Year.ToString(), r.ArtistName);
@ -137,11 +133,9 @@ namespace PlexRequests.Services.Jobs
continue;
}
Log.Trace("The result from Plex where the title's match was null, so that means the content is not yet in Plex.");
}
Log.Trace("Updating the requests now");
Log.Trace("Requests that will be updated count {0}", modifiedModel.Count);
Log.Debug("Requests that will be updated count {0}", modifiedModel.Count);
if (modifiedModel.Any())
{
@ -167,19 +161,36 @@ namespace PlexRequests.Services.Jobs
foreach (var lib in movieLibs)
{
movies.AddRange(lib.Video.Select(x => new PlexMovie() // movies are in the Video list
movies.AddRange(lib.Video.Select(video => new PlexMovie
{
Title = x.Title,
ReleaseYear = x.Year
ReleaseYear = video.Year,
Title = video.Title,
ProviderId = video.ProviderId,
}));
}
}
return movies;
}
public bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year)
public bool IsMovieAvailable(PlexMovie[] plexMovies, string title, string year, string providerId = null)
{
var advanced = !string.IsNullOrEmpty(providerId);
foreach (var movie in plexMovies)
{
return plexMovies.Any(x => x.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) && x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase));
if (advanced)
{
if (movie.ProviderId.Equals(providerId, StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
}
if (movie.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) &&
movie.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase))
{
return true;
}
}
return false;
}
public List<PlexTvShow> GetPlexTvShows()
@ -199,18 +210,33 @@ namespace PlexRequests.Services.Jobs
shows.AddRange(lib.Directory.Select(x => new PlexTvShow() // shows are in the directory list
{
Title = x.Title,
ReleaseYear = x.Year
ReleaseYear = x.Year,
ProviderId = x.ProviderId,
}));
}
}
return shows;
}
public bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year)
public bool IsTvShowAvailable(PlexTvShow[] plexShows, string title, string year, string providerId = null)
{
var advanced = !string.IsNullOrEmpty(providerId);
foreach (var show in plexShows)
{
if (advanced)
{
if (show.ProviderId.Equals(providerId, StringComparison.InvariantCultureIgnoreCase))
{
return plexShows.Any(x =>
(x.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) || x.Title.StartsWith(title, StringComparison.CurrentCultureIgnoreCase)) &&
x.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase));
return true;
}
}
if (show.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) &&
show.ReleaseYear.Equals(year, StringComparison.CurrentCultureIgnoreCase))
{
return true;
}
}
return false;
}
public List<PlexAlbum> GetPlexAlbums()
@ -248,24 +274,41 @@ namespace PlexRequests.Services.Jobs
private List<PlexSearch> CachedLibraries(AuthenticationSettings authSettings, PlexSettings plexSettings, bool setCache)
{
Log.Trace("Obtaining library sections from Plex");
List<PlexSearch> results = new List<PlexSearch>();
var results = new List<PlexSearch>();
if (!ValidateSettings(plexSettings, authSettings))
{
Log.Warn("The settings are not configured");
return results; // don't error out here, just let it go!
return results; // don't error out here, just let it go! let it goo!!!
}
try
{
if (setCache)
{
Log.Trace("Plex Lib API Call");
results = GetLibraries(authSettings, plexSettings);
Log.Trace("Plex Lib Cache Set Call");
if (plexSettings.AdvancedSearch)
{
for (var i = 0; i < results.Count; i++)
{
for (var j = 0; j < results[i].Directory.Count; j++)
{
var currentItem = results[i].Directory[j];
var metaData = PlexApi.GetMetadata(authSettings.PlexAuthToken, plexSettings.FullUri,
currentItem.RatingKey);
var providerId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Directory.Guid);
results[i].Directory[j].ProviderId = providerId;
}
for (var j = 0; j < results[i].Video.Count; j++)
{
var currentItem = results[i].Video[j];
var metaData = PlexApi.GetMetadata(authSettings.PlexAuthToken, plexSettings.FullUri,
currentItem.RatingKey);
var providerId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Video.Guid);
results[i].Video[j].ProviderId = providerId;
}
}
}
if (results != null)
{
Cache.Set(CacheKeys.PlexLibaries, results, CacheKeys.TimeFrameMinutes.SchedulerCaching);
@ -273,12 +316,8 @@ namespace PlexRequests.Services.Jobs
}
else
{
Log.Trace("Plex Lib GetSet Call");
results = Cache.GetOrSet(CacheKeys.PlexLibaries, () =>
{
Log.Trace("Plex Lib API Call (inside getset)");
return GetLibraries(authSettings, plexSettings);
}, CacheKeys.TimeFrameMinutes.SchedulerCaching);
GetLibraries(authSettings, plexSettings), CacheKeys.TimeFrameMinutes.SchedulerCaching);
}
}
catch (Exception ex)
@ -298,7 +337,6 @@ namespace PlexRequests.Services.Jobs
{
foreach (var dir in sections.Directories)
{
Log.Trace("Obtaining results from Plex for the following library section: {0}", dir.Title);
var lib = PlexApi.GetLibrary(authSettings.PlexAuthToken, plexSettings.FullUri, dir.Key);
if (lib != null)
{
@ -307,7 +345,6 @@ namespace PlexRequests.Services.Jobs
}
}
Log.Trace("Returning Plex Libs");
return libs;
}
@ -335,7 +372,6 @@ namespace PlexRequests.Services.Jobs
foreach (var model in modelChanged)
{
var selectedUsers = users.Select(x => x.Username).Intersect(model.RequestedUsers);
Log.Debug("Selected Users {0}", selectedUsers.DumpJson());
foreach (var user in selectedUsers)
{
Log.Info("Notifying user {0}", user);

@ -26,6 +26,7 @@
#endregion
using System;
using System.IO;
using System.Linq;
using NLog;
@ -130,7 +131,18 @@ namespace PlexRequests.Services.Jobs
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.
var last = files.LastOrDefault();
if (!string.IsNullOrEmpty(last))
{
var dt = ParseName(Path.GetFileName(last));
if (dt < DateTime.Now.AddHours(-1))
{
return true;
}
return false;
}
// We don't have a backup
return true;
}
@ -140,7 +152,6 @@ namespace PlexRequests.Services.Jobs
if (names.Length > 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;

@ -25,6 +25,7 @@
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
@ -51,14 +52,21 @@ namespace PlexRequests.Services.Jobs
private IRepository<LogEntity> Repo { get; }
private const int ItemsToDelete = 1000;
private void Cleanup()
{
try
{
var items = Repo.GetAll();
var orderedItems = items.Where(x => x.Date < DateTime.Now.AddDays(-7));
var ordered = items.OrderByDescending(x => x.Date).ToList();
var itemsToDelete = new List<LogEntity>();
if (ordered.Count > ItemsToDelete)
{
itemsToDelete = ordered.Skip(ItemsToDelete).ToList();
}
foreach (var o in orderedItems)
foreach (var o in itemsToDelete)
{
Repo.Delete(o);
}

@ -4,5 +4,6 @@
{
public string Title { get; set; }
public string ReleaseYear { get; set; }
public string ProviderId { get; set; }
}
}

@ -4,5 +4,6 @@
{
public string Title { get; set; }
public string ReleaseYear { get; set; }
public string ProviderId { get; set; }
}
}

@ -32,6 +32,10 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Build.Framework" />
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.3.4\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Security" />
@ -61,9 +65,6 @@
<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>

@ -4,6 +4,6 @@
<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="NLog" version="4.3.4" targetFramework="net45" />
<package id="Quartz" version="2.3.3" targetFramework="net45" />
</packages>

@ -0,0 +1,38 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: RequestBlobs.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("IssueBlobs")]
public class IssueBlobs : Entity
{
public int RequestId { get; set; }
public byte[] Content { get; set; }
public RequestType Type { get; set; }
}
}

@ -42,6 +42,10 @@
<Reference Include="Mono.Data.Sqlite">
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.3.4\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data.Linq" />
@ -55,13 +59,11 @@
<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\IssueBlobs.cs" />
<Compile Include="Models\ScheduledJobs.cs" />
<Compile Include="Models\UsersToNotify.cs" />
<Compile Include="Repository\BaseGenericRepository.cs" />
@ -89,6 +91,7 @@
<Compile Include="Models\Audit.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="sqlite3.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>

@ -83,8 +83,6 @@ namespace PlexRequests.Store.Repository
public bool Update(T entity)
{
ResetCache();
Log.Trace("Updating entity");
Log.Trace(entity.DumpJson());
using (var db = Config.DbConnection())
{
db.Open();
@ -95,8 +93,6 @@ namespace PlexRequests.Store.Repository
public async Task<bool> UpdateAsync(T entity)
{
ResetCache();
Log.Trace("Updating entity");
Log.Trace(entity.DumpJson());
using (var db = Config.DbConnection())
{
db.Open();

@ -17,6 +17,7 @@ namespace PlexRequests.Store
// ReSharper disable once IdentifierTypo
public int ProviderId { get; set; }
public string ImdbId { get; set; }
public string TvDbId { get; set; }
public string Overview { get; set; }
public string Title { get; set; }
public string PosterPath { get; set; }
@ -40,6 +41,7 @@ namespace PlexRequests.Store
public List<string> RequestedUsers { get; set; }
public string ArtistName { get; set; }
public string ArtistId { get; set; }
public int IssueId { get; set; }
[JsonIgnore]
public List<string> AllUsers
@ -83,6 +85,6 @@ namespace PlexRequests.Store
NoSubtitles = 1,
WrongContent = 2,
PlaybackIssues = 3,
Other = 4 // Provide a message
Other = 4, // Provide a message
}
}

@ -73,3 +73,12 @@ CREATE TABLE IF NOT EXISTS UsersToNotify
Username varchar(100) NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS UsersToNotify_Id ON UsersToNotify (Id);
CREATE TABLE IF NOT EXISTS IssueBlobs
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
RequestId INTEGER,
Type INTEGER NOT NULL,
Content BLOB NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS IssueBlobs_Id ON IssueBlobs (Id);

@ -0,0 +1,11 @@
<?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" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -3,5 +3,5 @@
<package id="Dapper" version="1.50.0-beta8" targetFramework="net45" />
<package id="Dapper.Contrib" version="1.50.0-beta8" targetFramework="net45" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net45" />
<package id="NLog" version="4.2.3" targetFramework="net45" />
<package id="NLog" version="4.3.4" targetFramework="net45" />
</packages>

@ -60,6 +60,7 @@ namespace PlexRequests.UI.Tests
private Mock<ISettingsService<PlexSettings>> PlexSettingsMock { get; set; }
private Mock<ISettingsService<SonarrSettings>> SonarrSettingsMock { get; set; }
private Mock<ISettingsService<SickRageSettings>> SickRageSettingsMock { get; set; }
private Mock<ISettingsService<ScheduledJobsSettings>> ScheduledJobsSettingsMock { get; set; }
private Mock<ISettingsService<EmailNotificationSettings>> EmailMock { get; set; }
private Mock<ISettingsService<PushbulletNotificationSettings>> PushbulletSettings { get; set; }
private Mock<ISettingsService<PushoverNotificationSettings>> PushoverSettings { get; set; }
@ -69,11 +70,13 @@ namespace PlexRequests.UI.Tests
private Mock<IPushbulletApi> PushbulletApi { get; set; }
private Mock<IPushoverApi> PushoverApi { get; set; }
private Mock<ICouchPotatoApi> CpApi { get; set; }
private Mock<IJobRecord> RecorderMock { get; set; }
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 Mock<ISettingsService<SlackNotificationSettings>> SlackSettings { get; set; }
private Mock<ISettingsService<LandingPageSettings>> LandingPageSettings { get; set; }
private Mock<ISlackApi> SlackApi { get; set; }
private ConfigurableBootstrapper Bootstrapper { get; set; }
@ -109,6 +112,10 @@ namespace PlexRequests.UI.Tests
Log = new Mock<ISettingsService<LogSettings>>();
SlackApi = new Mock<ISlackApi>();
SlackSettings = new Mock<ISettingsService<SlackNotificationSettings>>();
LandingPageSettings = new Mock<ISettingsService<LandingPageSettings>>();
ScheduledJobsSettingsMock = new Mock<ISettingsService<ScheduledJobsSettings>>();
RecorderMock = new Mock<IJobRecord>();
Bootstrapper = new ConfigurableBootstrapper(with =>
{
@ -133,7 +140,10 @@ namespace PlexRequests.UI.Tests
with.Dependency(Cache.Object);
with.Dependency(Log.Object);
with.Dependency(SlackApi.Object);
with.Dependency(LandingPageSettings.Object);
with.Dependency(SlackSettings.Object);
with.Dependency(ScheduledJobsSettingsMock.Object);
with.Dependency(RecorderMock.Object);
with.RootPathProvider<TestRootPathProvider>();
with.RequestStartup((container, pipelines, context) =>
{

@ -0,0 +1,57 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: LandingPageTests.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 NUnit.Framework;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Tests
{
[TestFixture]
public class LandingPageTests
{
[TestCaseSource(nameof(NoticeEnabledData))]
public bool TestNoticeEnabled(DateTime start, DateTime end)
{
return new LandingPageViewModel { NoticeEnd = end, NoticeStart = start }.NoticeActive;
}
private static IEnumerable<TestCaseData> NoticeEnabledData
{
get
{
yield return new TestCaseData(DateTime.Now, DateTime.Now.AddDays(1)).Returns(true);
yield return new TestCaseData(DateTime.Now, DateTime.Now.AddDays(99)).Returns(true);
yield return new TestCaseData(DateTime.Now.AddDays(2), DateTime.Now).Returns(false); // End in past
yield return new TestCaseData(DateTime.Now.AddDays(2), DateTime.Now.AddDays(3)).Returns(false); // Not started yet
yield return new TestCaseData(DateTime.Now.AddDays(-5), DateTime.Now.AddDays(-1)).Returns(false); // Finished yesterday
}
}
}
}

@ -101,7 +101,9 @@
<ItemGroup>
<Compile Include="ApiModuleTests.cs" />
<Compile Include="BootstrapperExtensions.cs" />
<Compile Include="LandingPageTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="StringHelperTests.cs" />
<Compile Include="TestRootPathProvider.cs" />
<Compile Include="UserLoginModuleTests.cs" />
<Compile Include="AdminModuleTests.cs" />

@ -0,0 +1,76 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: StringHelperTests.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using NUnit.Framework;
using PlexRequests.Core.Models;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Tests
{
[TestFixture]
public class StringHelperTests
{
[TestCaseSource(nameof(StringData))]
public string FirstCharToUpperTest(string input)
{
return input.FirstCharToUpper();
}
[TestCaseSource(nameof(StringCaseData))]
public string ToCamelCaseWordsTest(string input)
{
return input.ToCamelCaseWords();
}
private static IEnumerable<TestCaseData> StringData
{
get
{
yield return new TestCaseData("abcCba").Returns("AbcCba");
yield return new TestCaseData("").Returns("");
yield return new TestCaseData("12DSAda").Returns("12DSAda");
}
}
private static IEnumerable<TestCaseData> StringCaseData
{
get
{
yield return new TestCaseData("abcCba").Returns("Abc Cba");
yield return new TestCaseData("").Returns("");
yield return new TestCaseData("JamieRees").Returns("Jamie Rees");
yield return new TestCaseData("Jamierees").Returns("Jamierees");
yield return new TestCaseData("ThisIsANewString").Returns("This Is A New String");
yield return new TestCaseData("").Returns("");
yield return new TestCaseData(IssueStatus.PendingIssue.ToString()).Returns("Pending Issue");
yield return new TestCaseData(IssueStatus.ResolvedIssue.ToString()).Returns("Resolved Issue");
}
}
}
}

@ -25,15 +25,11 @@
// ************************************************************************/
#endregion
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Nancy;
using PlexRequests.UI.Modules;
namespace PlexRequests.UI.Tests
{
public class TestRootPathProvider : IRootPathProvider
@ -42,7 +38,6 @@ namespace PlexRequests.UI.Tests
public string GetRootPath()
{
//return @"C:\Applications\51\DeliveryDateCalculator\StandAndDeliver\Views\Home\";
if (!string.IsNullOrEmpty(_CachedRootPath))
return _CachedRootPath;

@ -25,6 +25,7 @@
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
@ -45,11 +46,11 @@ using PlexRequests.UI.Modules;
namespace PlexRequests.UI.Tests
{
[TestFixture]
//[Ignore("Needs some work")]
public class UserLoginModuleTests
{
private Mock<ISettingsService<AuthenticationSettings>> AuthMock { get; set; }
private Mock<ISettingsService<PlexRequestSettings>> PlexRequestMock { get; set; }
private Mock<ISettingsService<LandingPageSettings>> LandingPageMock { get; set; }
private ConfigurableBootstrapper Bootstrapper { get; set; }
private Mock<IPlexApi> PlexMock { get; set; }
@ -58,14 +59,18 @@ namespace PlexRequests.UI.Tests
{
AuthMock = new Mock<ISettingsService<AuthenticationSettings>>();
PlexMock = new Mock<IPlexApi>();
LandingPageMock = new Mock<ISettingsService<LandingPageSettings>>();
PlexRequestMock = new Mock<ISettingsService<PlexRequestSettings>>();
PlexRequestMock.Setup(x => x.GetSettings()).Returns(new PlexRequestSettings());
PlexRequestMock.Setup(x => x.GetSettingsAsync()).Returns(Task.FromResult(new PlexRequestSettings()));
LandingPageMock.Setup(x => x.GetSettings()).Returns(new LandingPageSettings());
Bootstrapper = new ConfigurableBootstrapper(with =>
{
with.Module<UserLoginModule>();
with.Dependency(PlexRequestMock.Object);
with.Dependency(AuthMock.Object);
with.Dependency(PlexMock.Object);
with.Dependency(LandingPageMock.Object);
with.RootPathProvider<TestRootPathProvider>();
});
}
@ -76,8 +81,6 @@ namespace PlexRequests.UI.Tests
var expectedSettings = new AuthenticationSettings { UserAuthentication = false, PlexAuthToken = "abc" };
AuthMock.Setup(x => x.GetSettings()).Returns(expectedSettings);
Bootstrapper.WithSession(new Dictionary<string, object>());
var browser = new Browser(Bootstrapper);

@ -52,9 +52,12 @@ using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using Nancy.Json;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Services.Jobs;
using PlexRequests.UI.Jobs;
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
namespace PlexRequests.UI
@ -65,74 +68,13 @@ namespace PlexRequests.UI
// by overriding the various methods and properties.
// For more information https://github.com/NancyFx/Nancy/wiki/Bootstrapper
protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context)
{
container.Register<ICacheProvider, MemoryCacheProvider>().AsSingleton();
// Settings
container.Register<ISettingsService<PlexRequestSettings>, SettingsServiceV2<PlexRequestSettings>>();
container.Register<ISettingsService<CouchPotatoSettings>, SettingsServiceV2<CouchPotatoSettings>>();
container.Register<ISettingsService<AuthenticationSettings>, SettingsServiceV2<AuthenticationSettings>>();
container.Register<ISettingsService<PlexSettings>, SettingsServiceV2<PlexSettings>>();
container.Register<ISettingsService<SonarrSettings>, SettingsServiceV2<SonarrSettings>>();
container.Register<ISettingsService<SickRageSettings>, SettingsServiceV2<SickRageSettings>>();
container.Register<ISettingsService<EmailNotificationSettings>, SettingsServiceV2<EmailNotificationSettings>>();
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<IJobFactory, CustomJobFactory>();
// Api's
container.Register<ICouchPotatoApi, CouchPotatoApi>();
container.Register<IPushbulletApi, PushbulletApi>();
container.Register<IPushoverApi, PushoverApi>();
container.Register<ISickRageApi, SickrageApi>();
container.Register<ISonarrApi, SonarrApi>();
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>();
ConfigureContainer(container);
JsonSettings.MaxJsonLength = int.MaxValue;
CookieBasedSessions.Enable(pipelines, CryptographyConfiguration.Default);
StaticConfiguration.DisableErrorTraces = false;
base.ApplicationStartup(container, pipelines);
@ -154,13 +96,15 @@ namespace PlexRequests.UI
ServicePointManager.ServerCertificateValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) => true;
SubscribeAllObservers(container);
}
protected override void ConfigureConventions(NancyConventions nancyConventions)
{
base.ConfigureConventions(nancyConventions);
var settings = new SettingsServiceV2<PlexRequestSettings>(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()),new MemoryCacheProvider()));
var settings = new SettingsServiceV2<PlexRequestSettings>(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider()));
var assetLocation = settings.GetSettings().BaseUrl;
nancyConventions.StaticContentsConventions.Add(
StaticContentConventionBuilder.AddDirectory($"{assetLocation}/Content", "Content")
@ -216,5 +160,69 @@ namespace PlexRequests.UI
});
base.RequestStartup(container, pipelines, context);
}
private void ConfigureContainer(TinyIoCContainer container)
{
container.Register<ICacheProvider, MemoryCacheProvider>().AsSingleton();
container.Register<ISqliteConfiguration, DbConfiguration>(new DbConfiguration(new SqliteFactory()));
container.Register<IRepository<UsersModel>, UserRepository<UsersModel>>();
container.Register<IUserMapper, UserMapper>();
container.Register<ICustomUserMapper, UserMapper>();
container.Register<ISettingsService<EmailNotificationSettings>, SettingsServiceV2<EmailNotificationSettings>>();
container.Register<ISettingsService<PushbulletNotificationSettings>, SettingsServiceV2<PushbulletNotificationSettings>>();
container.Register<ISettingsService<PushoverNotificationSettings>, SettingsServiceV2<PushoverNotificationSettings>>();
container.Register<ISettingsService<SlackNotificationSettings>, SettingsServiceV2<SlackNotificationSettings>>();
container.Register<ISettingsService<ScheduledJobsSettings>, SettingsServiceV2<ScheduledJobsSettings>>();
// Notification Service
container.Register<INotificationService, NotificationService>().AsSingleton();
// Settings
container.Register<ISettingsService<PlexRequestSettings>, SettingsServiceV2<PlexRequestSettings>>();
container.Register<ISettingsService<CouchPotatoSettings>, SettingsServiceV2<CouchPotatoSettings>>();
container.Register<ISettingsService<AuthenticationSettings>, SettingsServiceV2<AuthenticationSettings>>();
container.Register<ISettingsService<PlexSettings>, SettingsServiceV2<PlexSettings>>();
container.Register<ISettingsService<SonarrSettings>, SettingsServiceV2<SonarrSettings>>();
container.Register<ISettingsService<SickRageSettings>, SettingsServiceV2<SickRageSettings>>();
container.Register<ISettingsService<HeadphonesSettings>, SettingsServiceV2<HeadphonesSettings>>();
container.Register<ISettingsService<LogSettings>, SettingsServiceV2<LogSettings>>();
container.Register<ISettingsService<LandingPageSettings>, SettingsServiceV2<LandingPageSettings>>();
// Repo's
container.Register<IRepository<LogEntity>, GenericRepository<LogEntity>>();
container.Register<IRepository<UsersToNotify>, GenericRepository<UsersToNotify>>();
container.Register<IRepository<ScheduledJobs>, GenericRepository<ScheduledJobs>>();
container.Register<IRepository<IssueBlobs>, GenericRepository<IssueBlobs>>();
container.Register<IRequestService, JsonRequestModelRequestService>();
container.Register<IIssueService, IssueJsonService>();
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<IJobFactory, CustomJobFactory>();
container.Register<IAnalytics, Analytics>();
container.Register<ISchedulerFactory, StdSchedulerFactory>();
container.Register<IJobScheduler, Scheduler>();
// Api
container.Register<ICouchPotatoApi, CouchPotatoApi>();
container.Register<IPushbulletApi, PushbulletApi>();
container.Register<IPushoverApi, PushoverApi>();
container.Register<ISickRageApi, SickrageApi>();
container.Register<ISonarrApi, SonarrApi>();
container.Register<IPlexApi, PlexApi>();
container.Register<IMusicBrainzApi, MusicBrainzApi>();
container.Register<IHeadphonesApi, HeadphonesApi>();
container.Register<ISlackApi, SlackApi>();
var loc = ServiceLocator.Instance;
loc.SetContainer(container);
}
}
}

@ -6,7 +6,7 @@
.nav-tabs > li.active > a:focus {
background: #df691a; }
scroll-top-wrapper {
.scroll-top-wrapper {
background-color: #333333; }
.scroll-top-wrapper:hover {
@ -156,3 +156,23 @@ button.list-group-item:focus {
background-clip: padding-box;
outline: 0; }
.badge {
display: inline-block;
min-width: 10px;
padding: 3px 7px;
font-size: 12px;
font-weight: 300;
color: #ebebeb;
line-height: 1;
vertical-align: middle;
white-space: nowrap;
text-align: center;
background-color: #333333;
border-radius: 10px; }
.bootstrap-datetimepicker-widget.dropdown-menu {
background-color: #333333; }
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-bottom: 6px solid #333333 !important; }

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

@ -13,7 +13,7 @@ $i: !important;
background: $primary-colour;
}
scroll-top-wrapper {
.scroll-top-wrapper {
background-color: $bg-colour;
}
@ -194,3 +194,26 @@ button.list-group-item:focus {
background-clip: padding-box;
outline: 0;
}
.badge {
display: inline-block;
min-width: 10px;
padding: 3px 7px;
font-size: 12px;
font-weight: 300;
color: #ebebeb;
line-height: 1;
vertical-align: middle;
white-space: nowrap;
text-align: center;
background-color: $bg-colour;
border-radius: 10px;
}
.bootstrap-datetimepicker-widget.dropdown-menu {
background-color: $bg-colour;
}
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-bottom: 6px solid $bg-colour $i;
}

@ -0,0 +1,7 @@
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-77083919-2', 'auto');
ga('send', 'pageview');

@ -1,250 +0,0 @@
//
// 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;
}
}
}

@ -4,7 +4,9 @@
.bottom-align-text {
position: absolute;
bottom: 0;
right: 0; } }
right: 0; }
.landing-block .media {
max-width: 450px; } }
@media (max-width: 48em) {
.home {
@ -294,3 +296,36 @@ label {
margin-right: 15px;
margin-left: 15px; }
.bootstrap-datetimepicker-widget.dropdown-menu {
background-color: #4e5d6c; }
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-bottom: 6px solid #4e5d6c !important; }
.bootstrap-datetimepicker-widget table td.active,
.bootstrap-datetimepicker-widget table td.active:hover {
color: #fff !important; }
.landing-header {
display: block;
margin: 60px auto; }
.landing-block {
background: #2f2f2f !important;
padding: 5px; }
.landing-block .media {
margin: 30px auto;
max-width: 450px; }
.landing-block .media .media-left {
display: inline-block;
float: left;
width: 70px; }
.landing-block .media .media-left i.fa {
font-size: 3em; }
.landing-title {
font-weight: bold; }

File diff suppressed because one or more lines are too long

@ -6,7 +6,9 @@ $info-colour: #5bc0de;
$warning-colour: #f0ad4e;
$danger-colour: #d9534f;
$success-colour: #5cb85c;
$i: !important;
$i:
!important
;
@media (min-width: 768px ) {
.row {
@ -18,6 +20,10 @@ $i: !important;
bottom: 0;
right: 0;
}
.landing-block .media {
max-width: 450px;
}
}
@media (max-width: 48em) {
@ -330,9 +336,10 @@ $border-radius: 10px;
padding-left: 25px;
margin-right: 15px;
font-size: 13px;
margin-bottom: 10px; }
margin-bottom: 10px;
}
.checkbox label:before {
.checkbox label:before {
content: "";
display: inline-block;
width: 18px;
@ -342,23 +349,69 @@ $border-radius: 10px;
left: 0;
bottom: 1px;
border: 2px solid #eee;
border-radius: 3px; }
border-radius: 3px;
}
.checkbox input[type=checkbox] {
display: none; }
.checkbox input[type=checkbox] {
display: none;
}
.checkbox input[type=checkbox]:checked + label:before {
.checkbox input[type=checkbox]:checked + label:before {
content: "\2713";
font-size: 13px;
color: #fafafa;
text-align: center;
line-height: 13px; }
line-height: 13px;
}
.input-group-sm{
.input-group-sm {
padding-top: 2px;
padding-bottom: 2px;
}
.tab-pane .form-horizontal .form-group {
margin-right: 15px;
margin-left: 15px; }
margin-left: 15px;
}
.bootstrap-datetimepicker-widget.dropdown-menu {
background-color: $form-color;
}
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-bottom: 6px solid $form-color $i;
}
.bootstrap-datetimepicker-widget table td.active,
.bootstrap-datetimepicker-widget table td.active:hover {
color: #fff !important;
}
.landing-header {
display: block;
margin: 60px auto;
}
.landing-block {
background: #2f2f2f !important;
padding: 5px;
}
.landing-block .media {
margin: 30px auto;
max-width: 450px;
}
.landing-block .media .media-left {
display: inline-block;
float: left;
width: 70px;
}
.landing-block .media .media-left i.fa {
font-size: 3em;
}
.landing-title {
font-weight: bold;
}

@ -0,0 +1,17 @@
// Import bootstrap variables including default color palette and fonts
@import "bootstrap/less/variables.less";
// Import datepicker component
@import "_bootstrap-datetimepicker.less";
//this is here so the compiler doesn't complain about a missing bootstrap mixin
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,202 @@
/*!
* Datetimepicker for Bootstrap 3
* ! version : 4.7.14
* https://github.com/Eonasdan/bootstrap-datetimepicker/
*/
.sr-only, .bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after, .bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after, .bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after, .bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after, .bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after, .bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after, .bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after, .bootstrap-datetimepicker-widget .btn[data-action="clear"]::after, .bootstrap-datetimepicker-widget .btn[data-action="today"]::after, .bootstrap-datetimepicker-widget .picker-switch::after, .bootstrap-datetimepicker-widget table th.prev::after, .bootstrap-datetimepicker-widget table th.next::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0; }
.bootstrap-datetimepicker-widget {
list-style: none; }
.bootstrap-datetimepicker-widget.dropdown-menu {
margin: 2px 0;
padding: 4px;
width: 19em; }
@media (min-width: 768px) {
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
width: 38em; } }
@media (min-width: 768px) {
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
width: 38em; } }
@media (min-width: 768px) {
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
width: 38em; } }
.bootstrap-datetimepicker-widget.dropdown-menu:before, .bootstrap-datetimepicker-widget.dropdown-menu:after {
content: '';
display: inline-block;
position: absolute; }
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-bottom-color: rgba(0, 0, 0, 0.2);
top: -7px;
left: 7px; }
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid white;
top: -6px;
left: 8px; }
.bootstrap-datetimepicker-widget.dropdown-menu.top:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 7px solid #ccc;
border-top-color: rgba(0, 0, 0, 0.2);
bottom: -7px;
left: 6px; }
.bootstrap-datetimepicker-widget.dropdown-menu.top:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid white;
bottom: -6px;
left: 7px; }
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before {
left: auto;
right: 6px; }
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after {
left: auto;
right: 7px; }
.bootstrap-datetimepicker-widget .list-unstyled {
margin: 0; }
.bootstrap-datetimepicker-widget a[data-action] {
padding: 6px 0; }
.bootstrap-datetimepicker-widget a[data-action]:active {
box-shadow: none; }
.bootstrap-datetimepicker-widget .timepicker-hour, .bootstrap-datetimepicker-widget .timepicker-minute, .bootstrap-datetimepicker-widget .timepicker-second {
width: 54px;
font-weight: bold;
font-size: 1.2em;
margin: 0; }
.bootstrap-datetimepicker-widget button[data-action] {
padding: 6px; }
.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after {
content: "Increment Hours"; }
.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after {
content: "Increment Minutes"; }
.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after {
content: "Decrement Hours"; }
.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after {
content: "Decrement Minutes"; }
.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after {
content: "Show Hours"; }
.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after {
content: "Show Minutes"; }
.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after {
content: "Toggle AM/PM"; }
.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after {
content: "Clear the picker"; }
.bootstrap-datetimepicker-widget .btn[data-action="today"]::after {
content: "Set the date to today"; }
.bootstrap-datetimepicker-widget .picker-switch {
text-align: center; }
.bootstrap-datetimepicker-widget .picker-switch::after {
content: "Toggle Date and Time Screens"; }
.bootstrap-datetimepicker-widget .picker-switch td {
padding: 0;
margin: 0;
height: auto;
width: auto;
line-height: inherit; }
.bootstrap-datetimepicker-widget .picker-switch td span {
line-height: 2.5;
height: 2.5em;
width: 100%; }
.bootstrap-datetimepicker-widget table {
width: 100%;
margin: 0; }
.bootstrap-datetimepicker-widget table td,
.bootstrap-datetimepicker-widget table th {
text-align: center;
border-radius: #333333; }
.bootstrap-datetimepicker-widget table th {
height: 20px;
line-height: 20px;
width: 20px; }
.bootstrap-datetimepicker-widget table th.picker-switch {
width: 145px; }
.bootstrap-datetimepicker-widget table th.disabled, .bootstrap-datetimepicker-widget table th.disabled:hover {
background: none;
color: gray;
cursor: not-allowed; }
.bootstrap-datetimepicker-widget table th.prev::after {
content: "Previous Month"; }
.bootstrap-datetimepicker-widget table th.next::after {
content: "Next Month"; }
.bootstrap-datetimepicker-widget table thead tr:first-child th {
cursor: pointer; }
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover {
background: gray; }
.bootstrap-datetimepicker-widget table td {
height: 54px;
line-height: 54px;
width: 54px; }
.bootstrap-datetimepicker-widget table td.cw {
font-size: .8em;
height: 20px;
line-height: 20px;
color: gray; }
.bootstrap-datetimepicker-widget table td.day {
height: 20px;
line-height: 20px;
width: 20px; }
.bootstrap-datetimepicker-widget table td.day:hover, .bootstrap-datetimepicker-widget table td.hour:hover, .bootstrap-datetimepicker-widget table td.minute:hover, .bootstrap-datetimepicker-widget table td.second:hover {
background: gray;
cursor: pointer; }
.bootstrap-datetimepicker-widget table td.old, .bootstrap-datetimepicker-widget table td.new {
color: gray; }
.bootstrap-datetimepicker-widget table td.today {
position: relative; }
.bootstrap-datetimepicker-widget table td.today:before {
content: '';
display: inline-block;
border: 0 0 7px 7px solid transparent;
border-bottom-color: #df691a;
border-top-color: rgba(0, 0, 0, 0.2);
position: absolute;
bottom: 4px;
right: 4px; }
.bootstrap-datetimepicker-widget table td.active, .bootstrap-datetimepicker-widget table td.active:hover {
background-color: #df691a;
color: #ff761b;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); }
.bootstrap-datetimepicker-widget table td.active.today:before {
border-bottom-color: #fff; }
.bootstrap-datetimepicker-widget table td.disabled, .bootstrap-datetimepicker-widget table td.disabled:hover {
background: none;
color: gray;
cursor: not-allowed; }
.bootstrap-datetimepicker-widget table td span {
display: inline-block;
width: 54px;
height: 54px;
line-height: 54px;
margin: 2px 1.5px;
cursor: pointer;
border-radius: #333333; }
.bootstrap-datetimepicker-widget table td span:hover {
background: gray; }
.bootstrap-datetimepicker-widget table td span.active {
background-color: #df691a;
color: #ff761b;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); }
.bootstrap-datetimepicker-widget table td span.old {
color: gray; }
.bootstrap-datetimepicker-widget table td span.disabled, .bootstrap-datetimepicker-widget table td span.disabled:hover {
background: none;
color: gray;
cursor: not-allowed; }
.bootstrap-datetimepicker-widget.usetwentyfour td.hour {
height: 27px;
line-height: 27px; }
.input-group.date .input-group-addon {
cursor: pointer; }

File diff suppressed because one or more lines are too long

@ -0,0 +1,353 @@
/*!
* Datetimepicker for Bootstrap 3
* ! version : 4.7.14
* https://github.com/Eonasdan/bootstrap-datetimepicker/
*/
$bs-datetimepicker-timepicker-font-size: 1.2em !default;
$bs-datetimepicker-active-bg: #df691a !default;
$bs-datetimepicker-active-color: #ff761b !default;
$bs-datetimepicker-border-radius: #333333 !default;
$bs-datetimepicker-btn-hover-bg: gray !default;
$bs-datetimepicker-disabled-color: gray !default;
$bs-datetimepicker-alternate-color: gray !default;
$bs-datetimepicker-secondary-border-color: #ccc !default;
$bs-datetimepicker-secondary-border-color-rgba: rgba(0, 0, 0, 0.2) !default;
$bs-datetimepicker-primary-border-color: white !default;
$bs-datetimepicker-text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25) !default;
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
.bootstrap-datetimepicker-widget {
list-style: none;
&.dropdown-menu {
margin: 2px 0;
padding: 4px;
width: 19em;
&.timepicker-sbs {
@media (min-width: 768px) {
width: 38em;
}
@media (min-width: 768px) {
width: 38em;
}
@media (min-width: 768px) {
width: 38em;
}
}
&:before, &:after {
content: '';
display: inline-block;
position: absolute;
}
&.bottom {
&:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid $bs-datetimepicker-secondary-border-color;
border-bottom-color: $bs-datetimepicker-secondary-border-color-rgba;
top: -7px;
left: 7px;
}
&:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid $bs-datetimepicker-primary-border-color;
top: -6px;
left: 8px;
}
}
&.top {
&:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 7px solid $bs-datetimepicker-secondary-border-color;
border-top-color: $bs-datetimepicker-secondary-border-color-rgba;
bottom: -7px;
left: 6px;
}
&:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid $bs-datetimepicker-primary-border-color;
bottom: -6px;
left: 7px;
}
}
&.pull-right {
&:before {
left: auto;
right: 6px;
}
&:after {
left: auto;
right: 7px;
}
}
}
.list-unstyled {
margin: 0;
}
a[data-action] {
padding: 6px 0;
}
a[data-action]:active {
box-shadow: none;
}
.timepicker-hour, .timepicker-minute, .timepicker-second {
width: 54px;
font-weight: bold;
font-size: $bs-datetimepicker-timepicker-font-size;
margin: 0;
}
button[data-action] {
padding: 6px;
}
.btn[data-action="incrementHours"]::after {
@extend .sr-only;
content: "Increment Hours";
}
.btn[data-action="incrementMinutes"]::after {
@extend .sr-only;
content: "Increment Minutes";
}
.btn[data-action="decrementHours"]::after {
@extend .sr-only;
content: "Decrement Hours";
}
.btn[data-action="decrementMinutes"]::after {
@extend .sr-only;
content: "Decrement Minutes";
}
.btn[data-action="showHours"]::after {
@extend .sr-only;
content: "Show Hours";
}
.btn[data-action="showMinutes"]::after {
@extend .sr-only;
content: "Show Minutes";
}
.btn[data-action="togglePeriod"]::after {
@extend .sr-only;
content: "Toggle AM/PM";
}
.btn[data-action="clear"]::after {
@extend .sr-only;
content: "Clear the picker";
}
.btn[data-action="today"]::after {
@extend .sr-only;
content: "Set the date to today";
}
.picker-switch {
text-align: center;
&::after {
@extend .sr-only;
content: "Toggle Date and Time Screens";
}
td {
padding: 0;
margin: 0;
height: auto;
width: auto;
line-height: inherit;
span {
line-height: 2.5;
height: 2.5em;
width: 100%;
}
}
}
table {
width: 100%;
margin: 0;
& td,
& th {
text-align: center;
border-radius: $bs-datetimepicker-border-radius;
}
& th {
height: 20px;
line-height: 20px;
width: 20px;
&.picker-switch {
width: 145px;
}
&.disabled,
&.disabled:hover {
background: none;
color: $bs-datetimepicker-disabled-color;
cursor: not-allowed;
}
&.prev::after {
@extend .sr-only;
content: "Previous Month";
}
&.next::after {
@extend .sr-only;
content: "Next Month";
}
}
& thead tr:first-child th {
cursor: pointer;
&:hover {
background: $bs-datetimepicker-btn-hover-bg;
}
}
& td {
height: 54px;
line-height: 54px;
width: 54px;
&.cw {
font-size: .8em;
height: 20px;
line-height: 20px;
color: $bs-datetimepicker-alternate-color;
}
&.day {
height: 20px;
line-height: 20px;
width: 20px;
}
&.day:hover,
&.hour:hover,
&.minute:hover,
&.second:hover {
background: $bs-datetimepicker-btn-hover-bg;
cursor: pointer;
}
&.old,
&.new {
color: $bs-datetimepicker-alternate-color;
}
&.today {
position: relative;
&:before {
content: '';
display: inline-block;
border: 0 0 7px 7px solid transparent;
border-bottom-color: $bs-datetimepicker-active-bg;
border-top-color: $bs-datetimepicker-secondary-border-color-rgba;
position: absolute;
bottom: 4px;
right: 4px;
}
}
&.active,
&.active:hover {
background-color: $bs-datetimepicker-active-bg;
color: $bs-datetimepicker-active-color;
text-shadow: $bs-datetimepicker-text-shadow;
}
&.active.today:before {
border-bottom-color: #fff;
}
&.disabled,
&.disabled:hover {
background: none;
color: $bs-datetimepicker-disabled-color;
cursor: not-allowed;
}
span {
display: inline-block;
width: 54px;
height: 54px;
line-height: 54px;
margin: 2px 1.5px;
cursor: pointer;
border-radius: $bs-datetimepicker-border-radius;
&:hover {
background: $bs-datetimepicker-btn-hover-bg;
}
&.active {
background-color: $bs-datetimepicker-active-bg;
color: $bs-datetimepicker-active-color;
text-shadow: $bs-datetimepicker-text-shadow;
}
&.old {
color: $bs-datetimepicker-alternate-color;
}
&.disabled,
&.disabled:hover {
background: none;
color: $bs-datetimepicker-disabled-color;
cursor: not-allowed;
}
}
}
}
&.usetwentyfour {
td.hour {
height: 27px;
line-height: 27px;
}
}
}
.input-group.date {
& .input-group-addon {
cursor: pointer;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

@ -0,0 +1,46 @@
var base = $('#baseUrl').text();
// Note Modal click
$(".theNoteSaveButton").click(function (e) {
var comment = $("#noteArea").val();
e.preventDefault();
var $form = $("#noteForm");
var data = $form.serialize();
$.ajax({
type: $form.prop("method"),
url: $form.prop("action"),
data: data,
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
location.reload();
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
// Update the note modal
$('#noteModal').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 issue = button.data('issue');
var modal = $(this);
modal.find('.theNoteSaveButton').val(id); // Add ID to the button
var requestField = modal.find('.noteId');
requestField.val(id); // Add ID to the hidden field
var noteType = modal.find('.issue');
noteType.val(issue);
});

@ -0,0 +1,272 @@
Handlebars.registerHelper('if_eq', function (a, b, opts) {
if (a == b)
return !opts ? null : opts.fn(this);
else
return !opts ? null : opts.inverse(this);
});
var issueSource = $("#issue-template").html();
var issueTemplate = Handlebars.compile(issueSource);
var base = $('#baseUrl').text();
initLoad();
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var target = $(e.target).attr('href');
if (target === "#resolvedTab") {
loadResolvedIssues();
}
});
// Report Issue
$(document).on("click", ".dropdownIssue", function (e) {
var issue = $(this).attr("issue-select");
var id = e.target.id;
// Other issue so the modal is opening
if (issue == 4) {
return;
}
e.preventDefault();
var $form = $('#report' + id);
var data = $form.serialize();
data = data + "&issue=" + issue;
$.ajax({
type: $form.prop('method'),
url: $form.prop('action'),
data: data,
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! Added Issue.", "success");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
// Modal click
$(".theSaveButton").click(function (e) {
var comment = $("#commentArea").val();
e.preventDefault();
var $form = $("#commentForm");
var data = $form.serialize();
data = data + "&issue=" + 4 + "&comment=" + comment;
$.ajax({
type: $form.prop("method"),
url: $form.prop("action"),
data: data,
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! Added Issue.", "success");
$("#myModal").modal("hide");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
// Note Modal click
$(".theNoteSaveButton").click(function (e) {
var comment = $("#noteArea").val();
e.preventDefault();
var $form = $("#noteForm");
var data = $form.serialize();
$.ajax({
type: $form.prop("method"),
url: $form.prop("action"),
data: data,
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! Added Note.", "success");
$("#myModal").modal("hide");
$('#adminNotesArea' + e.target.value).html("<div>Note from Admin: " + comment + "</div>");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
// Update the modal
$('#myModal').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 modal = $(this);
modal.find('.theSaveButton').val(id); // Add ID to the button
var requestField = modal.find('input');
requestField.val(id); // Add ID to the hidden field
});
// Update the note modal
$('#noteModal').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 modal = $(this);
modal.find('.theNoteSaveButton').val(id); // Add ID to the button
var requestField = modal.find('.noteId');
requestField.val(id); // Add ID to the hidden field
});
// Delete
$(document).on("click", ".delete", function (e) {
e.preventDefault();
var buttonId = e.target.id;
var $form = $('#delete' + buttonId);
$.ajax({
type: $form.prop('method'),
url: $form.prop('action'),
data: $form.serialize(),
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! Request Deleted.", "success");
$("#" + buttonId + "Template").slideUp();
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
// Clear issues
$(document).on("click", ".clear", function (e) {
e.preventDefault();
var buttonId = e.target.id;
var $form = $('#clear' + buttonId);
$.ajax({
type: $form.prop('method'),
url: $form.prop('action'),
data: $form.serialize(),
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! Issues Cleared.", "info");
$('#issueArea' + buttonId).html("<div>Issue: None</div>");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
function initLoad() {
loadCounts();
loadPendingIssues();
}
function loadCounts() {
var url = createBaseUrl(base, "/issues/tabCount");
$.ajax({
type: "get",
url: url,
dataType: "json",
success: function (response) {
if (response.length > 0) {
response.forEach(function (result) {
if (result.count > 0) {
if (result.name == 0) {
$('#pendingCount').addClass("badge");
$('#pendingCount').html(result.count);
} else if (result.name == 1) {
$('#inProgressCount').addClass("badge");
$('#inProgressCount').html(result.count);
} else if (result.name == 2) {
$('#resolvedCount').addClass("badge");
$('#resolvedCount').html(result.count);
}
}
});
};
}
});
}
function loadPendingIssues() {
loadIssues("pending", $('#pendingIssues'));
}
function loadResolvedIssues() {
var $element = $('#resolvedIssues');
$element.html("");
loadIssues("resolved", $element);
}
function loadIssues(type, element) {
var url = createBaseUrl(base, "/issues/" + type);
var linkUrl = createBaseUrl(base, "/issues/");
$.ajax({
type: "get",
url: url,
dataType: "json",
success: function (response) {
if (response.length > 0) {
response.forEach(function (result) {
var context = buildIssueContext(result);
var html = issueTemplate(context);
element.append(html);
$("#" + result.id + "link").attr("href", linkUrl + result.id);
});
};
},
error: function (e) {
console.log(e);
generateNotify("Could not load Pending issues", "danger");
}
});
}
// Builds the issue context.
function buildIssueContext(result) {
var context = {
id: result.id,
requestId: result.requestId,
type: result.type,
title: result.title,
issues: result.issues
};
return context;
}

@ -190,6 +190,7 @@ $('#deleteMovies').click(function (e) {
}
});
});
$('#deleteTVShows').click(function (e) {
e.preventDefault();
if (!confirm("Are you sure you want to delete all TV show requests?")) return;
@ -223,6 +224,39 @@ $('#deleteTVShows').click(function (e) {
});
});
$('#deleteMusic').click(function (e) {
e.preventDefault();
if (!confirm("Are you sure you want to delete all album requests?")) return;
var buttonId = e.target.id;
var origHtml = $(this).html();
if ($('#' + buttonId).text() === " Loading...") {
return;
}
loadingButton(buttonId, "warning");
var url = createBaseUrl(base, '/approval/deletealltvshows');
$.ajax({
type: 'post',
url: url,
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! All TV Show requests deleted!", "success");
tvLoad();
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
},
complete: function (e) {
finishLoading(buttonId, "success", origHtml);
}
});
});
// filtering/sorting
$('.filter,.sort', '.dropdown-menu').click(function (e) {
var $this = $(this);
@ -411,31 +445,6 @@ $(document).on("click", ".approve-with-quality", function (e) {
});
// Clear issues
$(document).on("click", ".clear", function (e) {
e.preventDefault();
var buttonId = e.target.id;
var $form = $('#clear' + buttonId);
$.ajax({
type: $form.prop('method'),
url: $form.prop('action'),
data: $form.serialize(),
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! Issues Cleared.", "info");
$('#issueArea' + buttonId).html("<div>Issue: None</div>");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
// Change Availability
$(document).on("click", ".change", function (e) {
@ -620,10 +629,8 @@ function buildRequestContext(result, type) {
released: result.released,
available: result.available,
admin: result.admin,
issues: result.issues,
otherMessage: result.otherMessage,
issueId: result.issueId,
requestId: result.id,
adminNote: result.adminNotes,
imdb: result.imdbId,
seriesRequested: result.tvSeriesRequestType,
coverArtUrl: result.coverArtUrl,

@ -184,6 +184,78 @@ $(function () {
});
});
// Report Issue
$(document).on("click", ".dropdownIssue", function (e) {
var issue = $(this).attr("issue-select");
var id = e.target.id;
// Other issue so the modal is opening
if (issue == 4) {
return;
}
e.preventDefault();
var $form = $('#report' + id);
var data = $form.serialize();
data = data + "&issue=" + issue;
$.ajax({
type: $form.prop('method'),
url: $form.prop('action'),
data: data,
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Successfully Reported Issue.", "success");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
// Save Modal click
$(".theSaveButton").click(function (e) {
var comment = $("#commentArea").val();
e.preventDefault();
var $form = $("#commentForm");
var data = $form.serialize();
data = data + "&comment=" + comment;
$.ajax({
type: $form.prop("method"),
url: $form.prop("action"),
data: data,
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! Added Issue.", "success");
$("#myModal").modal("hide");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
// Update the modal
$('#issuesModal').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 type = button.data('type'); // Extract info from data-* attributes
var modal = $(this);
modal.find('.theSaveButton').val(id); // Add ID to the button
$('#providerIdModal').val(id);
$('#typeModal').val(type);
});
function focusSearch($content) {
if ($content.length > 0) {
$('input[type=text].form-control', $content).first().focus();

@ -14,6 +14,10 @@ function Humanize(date) {
return moment.duration(mNow - mDate).humanize() + (mNow.isBefore(mDate) ? ' from now' : ' ago');
}
function utcToLocal(date) {
return moment(date).local();
}
function generateNotify(message, type) {
// type = danger, warning, info, successs
$.notify({
@ -45,7 +49,7 @@ function loadingButton(elementId, originalCss) {
$element.removeClass("btn-" + originalCss + "-outline").addClass("btn-primary-outline").addClass('disabled').html("<i class='fa fa-spinner fa-spin'></i> Loading...");
// handle split-buttons
var $dropdown = $element.next('.dropdown-toggle')
var $dropdown = $element.next('.dropdown-toggle');
if ($dropdown.length > 0) {
$dropdown.removeClass("btn-" + originalCss + "-outline").addClass("btn-primary-outline").addClass('disabled');
}
@ -56,7 +60,7 @@ function finishLoading(elementId, originalCss, html) {
$element.removeClass("btn-primary-outline").removeClass('disabled').addClass("btn-" + originalCss + "-outline").html(html);
// handle split-buttons
var $dropdown = $element.next('.dropdown-toggle')
var $dropdown = $element.next('.dropdown-toggle');
if ($dropdown.length > 0) {
$dropdown.removeClass("btn-primary-outline").removeClass('disabled').addClass("btn-" + originalCss + "-outline");
}

@ -66,6 +66,7 @@ namespace PlexRequests.UI.Helpers
sb.AppendLine($"<link rel=\"stylesheet\" href=\"{content}/Content/awesome-bootstrap-checkbox.css\" type=\"text/css\"/>");
sb.AppendLine($"<link rel=\"stylesheet\" href=\"{content}/Content/base.css\" type=\"text/css\"/>");
sb.AppendLine($"<link rel=\"stylesheet\" href=\"{content}/Content/Themes/{settings.ThemeName}\" type=\"text/css\"/>");
sb.AppendLine($"<link rel=\"stylesheet\" href=\"{content}/Content/datepicker.min.css\" type=\"text/css\"/>");
sb.AppendLine($"<script src=\"{content}/Content/jquery-2.2.1.min.js\"></script>");
sb.AppendLine($"<script src=\"{content}/Content/handlebars.min.js\"></script>");
@ -75,6 +76,7 @@ namespace PlexRequests.UI.Helpers
sb.AppendLine($"<script src=\"{content}/Content/pace.min.js\"></script>");
sb.AppendLine($"<script src=\"{content}/Content/jquery.mixitup.js\"></script>");
sb.AppendLine($"<script src=\"{content}/Content/moment.min.js\"></script>");
sb.AppendLine($"<script src=\"{content}/Content/bootstrap-datetimepicker.min.js\"></script>");
return helper.Raw(sb.ToString());
@ -104,6 +106,28 @@ namespace PlexRequests.UI.Helpers
return helper.Raw(sb.ToString());
}
public static IHtmlString LoadIssueAssets(this HtmlHelpers helper)
{
var sb = new StringBuilder();
var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation);
sb.AppendLine($"<script src=\"{content}/Content/issues.js\" type=\"text/javascript\"></script>");
return helper.Raw(sb.ToString());
}
public static IHtmlString LoadIssueDetailsAssets(this HtmlHelpers helper)
{
var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation);
var asset = $"<script src=\"{content}/Content/issue-details.js\" type=\"text/javascript\"></script>";
return helper.Raw(asset);
}
public static IHtmlString LoadTableAssets(this HtmlHelpers helper)
{
var sb = new StringBuilder();
@ -117,6 +141,22 @@ namespace PlexRequests.UI.Helpers
return helper.Raw(sb.ToString());
}
public static IHtmlString LoadAnalytics(this HtmlHelpers helper)
{
var settings = GetSettings();
if (!settings.CollectAnalyticData)
{
return helper.Raw(string.Empty);
}
var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation);
var asset = $"<script src=\"{content}/Content/analytics.js\" type=\"text/javascript\"></script>";
return helper.Raw(asset);
}
public static IHtmlString GetSidebarUrl(this HtmlHelpers helper, NancyContext context, string url, string title)
{
var returnString = string.Empty;
@ -157,6 +197,27 @@ namespace PlexRequests.UI.Helpers
return helper.Raw(returnString);
}
public static IHtmlString GetNavbarUrl(this HtmlHelpers helper, NancyContext context, string url, string title, string fontIcon, string extraHtml)
{
var returnString = string.Empty;
var content = GetLinkUrl(GetBaseUrl());
if (!string.IsNullOrEmpty(content))
{
url = $"/{content}{url}";
}
if (context.Request.Path == url)
{
returnString = $"<li class=\"active\"><a href=\"{url}\"><i class=\"fa fa-{fontIcon}\"></i> {title} {extraHtml}</a></li>";
}
else
{
returnString = $"<li><a href=\"{url}\"><i class=\"fa fa-{fontIcon}\"></i> {title} {extraHtml}</a></li>";
}
return helper.Raw(returnString);
}
public static IHtmlString GetBaseUrl(this HtmlHelpers helper)
{
return helper.Raw(GetBaseUrl());

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

@ -1,4 +1,29 @@
using System;
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: StringHelper.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Linq;
using System.Text.RegularExpressions;
@ -8,14 +33,17 @@ namespace PlexRequests.UI.Helpers
{
public static string FirstCharToUpper(this string input)
{
if (String.IsNullOrEmpty(input))
if (string.IsNullOrEmpty(input))
return input;
return input.First().ToString().ToUpper() + String.Join("", input.Skip(1));
var firstUpper = char.ToUpper(input[0]);
return firstUpper + string.Join("", input.Skip(1));
}
public static string CamelCaseToWords(this string input)
public static string ToCamelCaseWords(this string input)
{
if (string.IsNullOrEmpty(input))
return input;
return Regex.Replace(input.FirstCharToUpper(), "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1 ");
}
}

@ -53,12 +53,16 @@ namespace PlexRequests.UI.Helpers
public SonarrAddSeries SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model, string qualityId)
{
int qualityProfile;
if (!string.IsNullOrEmpty(qualityId) || !int.TryParse(qualityId, out qualityProfile)) // try to parse the passed in quality, otherwise use the settings default quality
var qualityProfile = 0;
if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality
{
if (!int.TryParse(qualityId, out qualityProfile))
{
int.TryParse(sonarrSettings.QualityProfile, out qualityProfile);
}
}
var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey,
@ -83,16 +87,11 @@ namespace PlexRequests.UI.Helpers
qualityId = sickRageSettings.QualityProfile;
}
Log.Trace("Calling `AddSeries` with the following settings:");
Log.Trace(sickRageSettings.DumpJson());
Log.Trace("And the following `model`:");
Log.Trace(model.DumpJson());
var apiResult = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, qualityId,
sickRageSettings.ApiKey, sickRageSettings.FullUri);
var result = apiResult.Result;
Log.Trace("SickRage Add Result: ");
Log.Trace(result.DumpJson());
return result;
}

@ -30,16 +30,20 @@ using System.Linq;
using NLog;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Services.Jobs;
using PlexRequests.UI.Helpers;
using Quartz;
using Quartz.Impl;
namespace PlexRequests.UI.Jobs
{
internal sealed class Scheduler
internal sealed class Scheduler : IJobScheduler
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private IServiceLocator Service => ServiceLocator.Instance;
private readonly ISchedulerFactory _factory;
@ -99,48 +103,51 @@ namespace PlexRequests.UI.Jobs
private IEnumerable<ITrigger> CreateTriggers()
{
var settingsService = Service.Resolve<ISettingsService<ScheduledJobsSettings>>();
var s = settingsService.GetSettings();
var triggers = new List<ITrigger>();
var plexAvailabilityChecker =
TriggerBuilder.Create()
.WithIdentity("PlexAvailabilityChecker", "Plex")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(10).RepeatForever())
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexAvailabilityChecker).RepeatForever())
.Build();
var srCacher =
TriggerBuilder.Create()
.WithIdentity("SickRageCacher", "Cache")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(10).RepeatForever())
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SickRageCacher).RepeatForever())
.Build();
var sonarrCacher =
TriggerBuilder.Create()
.WithIdentity("SonarrCacher", "Cache")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(10).RepeatForever())
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SonarrCacher).RepeatForever())
.Build();
var cpCacher =
TriggerBuilder.Create()
.WithIdentity("CouchPotatoCacher", "Cache")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(10).RepeatForever())
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.CouchPotatoCacher).RepeatForever())
.Build();
var storeBackup =
TriggerBuilder.Create()
.WithIdentity("StoreBackup", "Database")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(24).RepeatForever())
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreBackup).RepeatForever())
.Build();
var storeCleanup =
TriggerBuilder.Create()
.WithIdentity("StoreCleanup", "Database")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(24).RepeatForever())
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreCleanup).RepeatForever())
.Build();
@ -154,4 +161,9 @@ namespace PlexRequests.UI.Jobs
return triggers;
}
}
public interface IJobScheduler
{
void StartScheduler();
}
}

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

@ -0,0 +1,41 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: IssuesViewModel.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 PlexRequests.Store;
namespace PlexRequests.UI.Models
{
public class IssuesViewModel
{
public int Id { get; set; }
public int RequestId { get; set; }
public string Title { get; set; }
public string Issues { get; set; }
public string Type { get; set; }
}
}

@ -0,0 +1,36 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: LandingPageViewModel.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 PlexRequests.Core.SettingModels;
namespace PlexRequests.UI.Models
{
public class LandingPageViewModel : LandingPageSettings
{
public string ContinueUrl { get; set; }
}
}

@ -49,9 +49,7 @@ namespace PlexRequests.UI.Models
public string ReleaseYear { get; set; }
public bool Available { get; set; }
public bool Admin { get; set; }
public string Issues { get; set; }
public string OtherMessage { get; set; }
public string AdminNotes { get; set; }
public int IssueId { get; set; }
public string TvSeriesRequestType { get; set; }
public string MusicBrainzId { get; set; }
public QualityModel[] Qualities { get; set; }

@ -0,0 +1,38 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: ScheduledJobsViewModel.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 PlexRequests.Core.SettingModels;
namespace PlexRequests.UI.Models
{
public class ScheduledJobsViewModel : ScheduledJobsSettings
{
public Dictionary<string,DateTime> JobRecorder { get; set; }
}
}

@ -23,41 +23,43 @@
// 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.Net;
using PlexRequests.Helpers.Exceptions;
#endregion
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using MarkdownSharp;
using System.Net;
using Nancy;
using Nancy.Extensions;
using Nancy.ModelBinding;
using Nancy.Responses.Negotiation;
using Nancy.Validation;
using Nancy.Json;
using Nancy.Security;
using NLog;
using MarkdownSharp;
using PlexRequests.Api;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using System;
using System.Diagnostics;
using Nancy.Json;
using Nancy.Security;
namespace PlexRequests.UI.Modules
{
@ -83,7 +85,10 @@ namespace PlexRequests.UI.Modules
private INotificationService NotificationService { get; }
private ICacheProvider Cache { get; }
private ISettingsService<SlackNotificationSettings> SlackSettings { get; }
private ISettingsService<LandingPageSettings> LandingSettings { get; }
private ISettingsService<ScheduledJobsSettings> ScheduledJobSettings { get; }
private ISlackApi SlackApi { get; }
private IJobRecord JobRecorder { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public AdminModule(ISettingsService<PlexRequestSettings> prService,
@ -105,7 +110,8 @@ namespace PlexRequests.UI.Modules
ISettingsService<HeadphonesSettings> headphones,
ISettingsService<LogSettings> logs,
ICacheProvider cache, ISettingsService<SlackNotificationSettings> slackSettings,
ISlackApi slackApi) : base("admin", prService)
ISlackApi slackApi, ISettingsService<LandingPageSettings> lp,
ISettingsService<ScheduledJobsSettings> scheduler, IJobRecord rec) : base("admin", prService)
{
PrService = prService;
CpService = cpService;
@ -128,13 +134,16 @@ namespace PlexRequests.UI.Modules
Cache = cache;
SlackSettings = slackSettings;
SlackApi = slackApi;
LandingSettings = lp;
ScheduledJobSettings = scheduler;
JobRecorder = rec;
this.RequiresClaims(UserClaims.Admin);
Get["/"] = _ => Admin();
Get["/authentication"] = _ => Authentication();
Post["/authentication"] = _ => SaveAuthentication();
Get["/authentication", true] = async (x, ct) => await Authentication();
Post["/authentication", true] = async (x, ct) => await SaveAuthentication();
Post["/"] = _ => SaveAdmin();
@ -160,7 +169,7 @@ namespace PlexRequests.UI.Modules
Get["/emailnotification"] = _ => EmailNotifications();
Post["/emailnotification"] = _ => SaveEmailNotifications();
Post["/testemailnotification"] = _ => TestEmailNotifications();
Get["/status"] = _ => Status();
Get["/status", true] = async (x,ct) => await Status();
Get["/pushbulletnotification"] = _ => PushbulletNotifications();
Post["/pushbulletnotification"] = _ => SavePushbulletNotifications();
@ -186,20 +195,26 @@ namespace PlexRequests.UI.Modules
Get["/slacknotification"] = _ => SlackNotifications();
Post["/slacknotification"] = _ => SaveSlackNotifications();
Get["/landingpage", true] = async (x, ct) => await LandingPage();
Post["/landingpage", true] = async (x, ct) => await SaveLandingPage();
Get["/scheduledjobs", true] = async (x, ct) => await GetScheduledJobs();
Post["/scheduledjobs", true] = async (x, ct) => await SaveScheduledJobs();
}
private Negotiator Authentication()
private async Task<Negotiator> Authentication()
{
var settings = AuthService.GetSettings();
var settings = await AuthService.GetSettingsAsync();
return View["/Authentication", settings];
}
private Response SaveAuthentication()
private async Task<Response> SaveAuthentication()
{
var model = this.Bind<AuthenticationSettings>();
var result = AuthService.SaveSettings(model);
var result = await AuthService.SaveSettingsAsync(model);
if (result)
{
if (!string.IsNullOrEmpty(BaseUrl))
@ -218,8 +233,6 @@ namespace PlexRequests.UI.Modules
private Negotiator Admin()
{
var settings = PrService.GetSettings();
Log.Trace("Getting Settings:");
Log.Trace(settings.DumpJson());
return View["Settings", settings];
}
@ -227,23 +240,23 @@ namespace PlexRequests.UI.Modules
private Response SaveAdmin()
{
var model = this.Bind<PlexRequestSettings>();
var valid = this.Validate (model);
if (!valid.IsValid) {
var valid = this.Validate(model);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
if (!string.IsNullOrWhiteSpace (model.BaseUrl)) {
if (model.BaseUrl.StartsWith ("/") || model.BaseUrl.StartsWith ("\\"))
if (!string.IsNullOrWhiteSpace(model.BaseUrl))
{
if (model.BaseUrl.StartsWith("/", StringComparison.CurrentCultureIgnoreCase) || model.BaseUrl.StartsWith("\\", StringComparison.CurrentCultureIgnoreCase))
{
model.BaseUrl = model.BaseUrl.Remove (0, 1);
model.BaseUrl = model.BaseUrl.Remove(0, 1);
}
}
var result = PrService.SaveSettings(model);
if (result) {
return Response.AsJson (new JsonResponseModel{ Result = true });
}
return Response.AsJson (new JsonResponseModel{ Result = false, Message = "We could not save to the database, please try again" });
return Response.AsJson(result
? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "We could not save to the database, please try again" });
}
private Response RequestAuthToken()
@ -291,7 +304,8 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new { Result = true, Users = string.Empty });
}
try {
try
{
var users = PlexApi.GetUsers(token);
if (users == null)
{
@ -303,28 +317,27 @@ namespace PlexRequests.UI.Modules
}
var usernames = users.User.Select(x => x.Title);
return Response.AsJson(new {Result = true, Users = usernames});
} catch (Exception ex) {
Log.Error (ex);
if (ex is WebException || ex is ApiRequestException) {
return Response.AsJson (new { Result = false, Message ="Could not load the user list! We have connectivity problems connecting to Plex, Please ensure we can access Plex.Tv, The error has been logged." });
return Response.AsJson(new { Result = true, Users = usernames });
}
catch (Exception ex)
{
Log.Error(ex);
if (ex is WebException || ex is ApiRequestException)
{
return Response.AsJson(new { Result = false, Message = "Could not load the user list! We have connectivity problems connecting to Plex, Please ensure we can access Plex.Tv, The error has been logged." });
}
return Response.AsJson (new { Result = false, Message = ex.Message});
return Response.AsJson(new { Result = false, Message = ex.Message });
}
}
private Negotiator CouchPotato()
{
dynamic model = new ExpandoObject();
var settings = CpService.GetSettings();
model = settings;
return View["CouchPotato", model];
return View["CouchPotato", settings];
}
private Response SaveCouchPotato()
{
var couchPotatoSettings = this.Bind<CouchPotatoSettings>();
@ -499,11 +512,11 @@ namespace PlexRequests.UI.Modules
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private Negotiator Status()
private async Task<Negotiator> Status()
{
var checker = new StatusChecker();
var status = checker.GetStatus();
var md = new Markdown(new MarkdownOptions { AutoNewLines = true });
var status = await Cache.GetOrSetAsync(CacheKeys.LastestProductVersion, async () => await checker.GetStatus(), 30);
var md = new Markdown(new MarkdownOptions { AutoNewLines = true, AutoHyperlink = true});
status.ReleaseNotes = md.Transform(status.ReleaseNotes);
return View["Status", status];
}
@ -536,7 +549,6 @@ namespace PlexRequests.UI.Modules
{
return Response.AsJson(valid.SendJsonError());
}
Log.Trace(settings.DumpJson());
var result = PushbulletService.SaveSettings(settings);
if (settings.Enabled)
@ -599,7 +611,6 @@ namespace PlexRequests.UI.Modules
{
return Response.AsJson(valid.SendJsonError());
}
Log.Trace(settings.DumpJson());
var result = PushoverService.SaveSettings(settings);
if (settings.Enabled)
@ -723,7 +734,6 @@ namespace PlexRequests.UI.Modules
Log.Info("Error validating Headphones settings, message: {0}", error.Message);
return Response.AsJson(error);
}
Log.Trace(settings.DumpJson());
var result = HeadphonesService.SaveSettings(settings);
@ -804,5 +814,63 @@ namespace PlexRequests.UI.Modules
? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Slack Notifications!" }
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private async Task<Negotiator> LandingPage()
{
var settings = await LandingSettings.GetSettingsAsync();
return View["LandingPage", settings];
}
private async Task<Response> SaveLandingPage()
{
var settings = this.Bind<LandingPageSettings>();
var plexSettings = await PlexService.GetSettingsAsync();
if (string.IsNullOrEmpty(plexSettings.Ip))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "We cannot enable the landing page if Plex is not setup!" });
}
if (settings.Enabled && settings.EnabledNoticeTime && string.IsNullOrEmpty(settings.NoticeMessage))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "If you are going to enabled the notice, then we need a message!" });
}
var result = await LandingSettings.SaveSettingsAsync(settings);
return Response.AsJson(result
? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "Could not save to Db Please check the logs" });
}
private async Task<Negotiator> GetScheduledJobs()
{
var s = await ScheduledJobSettings.GetSettingsAsync();
var allJobs = await JobRecorder.GetJobsAsync();
var jobsDict = allJobs.ToDictionary(k => k.Name, v => v.LastRun);
var model = new ScheduledJobsViewModel
{
CouchPotatoCacher = s.CouchPotatoCacher,
PlexAvailabilityChecker = s.PlexAvailabilityChecker,
SickRageCacher = s.SickRageCacher,
SonarrCacher = s.SonarrCacher,
StoreBackup = s.StoreBackup,
StoreCleanup = s.StoreCleanup,
JobRecorder = jobsDict
};
return View["SchedulerSettings", model];
}
private async Task<Response> SaveScheduledJobs()
{
var settings = this.Bind<ScheduledJobsSettings>();
var result = await ScheduledJobSettings.SaveSettingsAsync(settings);
return Response.AsJson(result
? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "Could not save to Db Please check the logs" });
}
}
}

@ -28,6 +28,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Nancy;
using Nancy.Security;
@ -62,12 +64,13 @@ namespace PlexRequests.UI.Modules
HeadphonesSettings = hpSettings;
HeadphoneApi = hpApi;
Post["/approve"] = parameters => Approve((int)Request.Form.requestid, (string)Request.Form.qualityId);
Post["/approveall"] = x => ApproveAll();
Post["/approveallmovies"] = x => ApproveAllMovies();
Post["/approvealltvshows"] = x => ApproveAllTVShows();
Post["/deleteallmovies"] = x => DeleteAllMovies();
Post["/deletealltvshows"] = x => DeleteAllTVShows();
Post["/approve", true] = async (x, ct) => await Approve((int)Request.Form.requestid, (string)Request.Form.qualityId);
Post["/approveall", true] = async (x, ct) => await ApproveAll();
Post["/approveallmovies", true] = async (x, ct) => await ApproveAllMovies();
Post["/approvealltvshows", true] = async (x, ct) => await ApproveAllTVShows();
Post["/deleteallmovies", true] = async (x, ct) => await DeleteAllMovies();
Post["/deletealltvshows", true] = async (x, ct) => await DeleteAllTVShows();
Post["/deleteallalbums", true] = async (x, ct) => await DeleteAllAlbums();
}
private IRequestService Service { get; }
@ -87,12 +90,12 @@ namespace PlexRequests.UI.Modules
/// </summary>
/// <param name="requestId">The request identifier.</param>
/// <returns></returns>
private Response Approve(int requestId, string qualityId)
private async Task<Response> Approve(int requestId, string qualityId)
{
Log.Info("approving request {0}", requestId);
// Get the request from the DB
var request = Service.Get(requestId);
var request = await Service.GetAsync(requestId);
if (request == null)
{
@ -103,21 +106,21 @@ namespace PlexRequests.UI.Modules
switch (request.Type)
{
case RequestType.Movie:
return RequestMovieAndUpdateStatus(request, qualityId);
return await RequestMovieAndUpdateStatus(request, qualityId);
case RequestType.TvShow:
return RequestTvAndUpdateStatus(request, qualityId);
return await RequestTvAndUpdateStatus(request, qualityId);
case RequestType.Album:
return RequestAlbumAndUpdateStatus(request);
return await RequestAlbumAndUpdateStatus(request);
default:
throw new ArgumentOutOfRangeException(nameof(request));
}
}
private Response RequestTvAndUpdateStatus(RequestedModel request, string qualityId)
private async Task<Response> RequestTvAndUpdateStatus(RequestedModel request, string qualityId)
{
var sender = new TvSender(SonarrApi, SickRageApi);
var sonarrSettings = SonarrSettings.GetSettings();
var sonarrSettings = await SonarrSettings.GetSettingsAsync();
if (sonarrSettings.Enabled)
{
Log.Trace("Sending to Sonarr");
@ -128,7 +131,7 @@ namespace PlexRequests.UI.Modules
{
Log.Info("Sent successfully, Approving request now.");
request.Approved = true;
var requestResult = Service.UpdateRequest(request);
var requestResult = await Service.UpdateRequestAsync(request);
Log.Trace("Approval result: {0}", requestResult);
if (requestResult)
{
@ -145,7 +148,7 @@ namespace PlexRequests.UI.Modules
}
var srSettings = SickRageSettings.GetSettings();
var srSettings = await SickRageSettings.GetSettingsAsync();
if (srSettings.Enabled)
{
Log.Trace("Sending to SickRage");
@ -156,7 +159,7 @@ namespace PlexRequests.UI.Modules
{
Log.Info("Sent successfully, Approving request now.");
request.Approved = true;
var requestResult = Service.UpdateRequest(request);
var requestResult = await Service.UpdateRequestAsync(request);
Log.Trace("Approval result: {0}", requestResult);
return Response.AsJson(requestResult
? new JsonResponseModel { Result = true }
@ -171,15 +174,15 @@ namespace PlexRequests.UI.Modules
request.Approved = true;
var res = Service.UpdateRequest(request);
var res = await Service.UpdateRequestAsync(request);
return Response.AsJson(res
? new JsonResponseModel { Result = true, Message = "This has been approved, but It has not been sent to Sonarr/SickRage because it has not been configured" }
: new JsonResponseModel { Result = false, Message = "Updated SickRage but could not approve it in PlexRequests :(" });
}
private Response RequestMovieAndUpdateStatus(RequestedModel request, string qualityId)
private async Task<Response> RequestMovieAndUpdateStatus(RequestedModel request, string qualityId)
{
var cpSettings = CpService.GetSettings();
var cpSettings = await CpService.GetSettingsAsync();
Log.Info("Adding movie to CouchPotato : {0}", request.Title);
if (!cpSettings.Enabled)
@ -189,7 +192,7 @@ namespace PlexRequests.UI.Modules
Log.Warn("We approved movie: {0} but could not add it to CouchPotato because it has not been setup", request.Title);
// Update the record
var inserted = Service.UpdateRequest(request);
var inserted = await Service.UpdateRequestAsync(request);
return Response.AsJson(inserted
? new JsonResponseModel { Result = true, Message = "This has been approved, but It has not been sent to CouchPotato because it has not been configured." }
: new JsonResponseModel
@ -207,7 +210,7 @@ namespace PlexRequests.UI.Modules
request.Approved = true;
// Update the record
var inserted = Service.UpdateRequest(request);
var inserted = await Service.UpdateRequestAsync(request);
return Response.AsJson(inserted
? new JsonResponseModel { Result = true }
@ -227,9 +230,9 @@ namespace PlexRequests.UI.Modules
});
}
private Response RequestAlbumAndUpdateStatus(RequestedModel request)
private async Task<Response> RequestAlbumAndUpdateStatus(RequestedModel request)
{
var hpSettings = HeadphonesSettings.GetSettings();
var hpSettings = await HeadphonesSettings.GetSettingsAsync();
Log.Info("Adding album to Headphones : {0}", request.Title);
if (!hpSettings.Enabled)
{
@ -238,7 +241,7 @@ namespace PlexRequests.UI.Modules
Log.Warn("We approved Album: {0} but could not add it to Headphones because it has not been setup", request.Title);
// Update the record
var inserted = Service.UpdateRequest(request);
var inserted = await Service.UpdateRequestAsync(request);
return Response.AsJson(inserted
? new JsonResponseModel { Result = true, Message = "This has been approved, but It has not been sent to Headphones because it has not been configured." }
: new JsonResponseModel
@ -255,10 +258,11 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = true, Message = "We have sent the approval to Headphones for processing, This can take a few minutes." });
}
private Response ApproveAllMovies()
private async Task<Response> ApproveAllMovies()
{
var requests = Service.GetAll().Where(x => x.CanApprove && x.Type == RequestType.Movie);
var requests = await Service.GetAllAsync();
requests = requests.Where(x => x.CanApprove && x.Type == RequestType.Movie);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any())
{
@ -267,7 +271,7 @@ namespace PlexRequests.UI.Modules
try
{
return UpdateRequests(requestedModels);
return await UpdateRequestsAsync(requestedModels);
}
catch (Exception e)
{
@ -276,10 +280,11 @@ namespace PlexRequests.UI.Modules
}
}
private Response DeleteAllMovies()
private async Task<Response> DeleteAllMovies()
{
var requests = Service.GetAll().Where(x => x.Type == RequestType.Movie);
var requests = await Service.GetAllAsync();
requests = requests.Where(x => x.Type == RequestType.Movie);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any())
{
@ -288,7 +293,29 @@ namespace PlexRequests.UI.Modules
try
{
return DeleteRequests(requestedModels);
return await DeleteRequestsAsync(requestedModels);
}
catch (Exception e)
{
Log.Fatal(e);
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" });
}
}
private async Task<Response> DeleteAllAlbums()
{
var requests = await Service.GetAllAsync();
requests = requests.Where(x => x.Type == RequestType.Album);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any())
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no album requests to delete. Please refresh." });
}
try
{
return await DeleteRequestsAsync(requestedModels);
}
catch (Exception e)
{
@ -297,9 +324,10 @@ namespace PlexRequests.UI.Modules
}
}
private Response ApproveAllTVShows()
private async Task<Response> ApproveAllTVShows()
{
var requests = Service.GetAll().Where(x => x.CanApprove && x.Type == RequestType.TvShow);
var requests = await Service.GetAllAsync();
requests = requests.Where(x => x.CanApprove && x.Type == RequestType.TvShow);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any())
{
@ -308,7 +336,7 @@ namespace PlexRequests.UI.Modules
try
{
return UpdateRequests(requestedModels);
return await UpdateRequestsAsync(requestedModels);
}
catch (Exception e)
{
@ -317,10 +345,11 @@ namespace PlexRequests.UI.Modules
}
}
private Response DeleteAllTVShows()
private async Task<Response> DeleteAllTVShows()
{
var requests = Service.GetAll().Where(x => x.Type == RequestType.TvShow);
var requests = await Service.GetAllAsync();
requests = requests.Where(x => x.Type == RequestType.TvShow);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any())
{
@ -329,7 +358,7 @@ namespace PlexRequests.UI.Modules
try
{
return DeleteRequests(requestedModels);
return await DeleteRequestsAsync(requestedModels);
}
catch (Exception e)
{
@ -342,9 +371,10 @@ namespace PlexRequests.UI.Modules
/// Approves all.
/// </summary>
/// <returns></returns>
private Response ApproveAll()
private async Task<Response> ApproveAll()
{
var requests = Service.GetAll().Where(x => x.CanApprove);
var requests = await Service.GetAllAsync();
requests = requests.Where(x => x.CanApprove);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any())
{
@ -353,7 +383,7 @@ namespace PlexRequests.UI.Modules
try
{
return UpdateRequests(requestedModels);
return await UpdateRequestsAsync(requestedModels);
}
catch (Exception e)
{
@ -363,11 +393,11 @@ namespace PlexRequests.UI.Modules
}
private Response DeleteRequests(RequestedModel[] requestedModels)
private async Task<Response> DeleteRequestsAsync(IEnumerable<RequestedModel> requestedModels)
{
try
{
var result = Service.BatchDelete(requestedModels.ToList());
var result = await Service.BatchDeleteAsync(requestedModels);
return Response.AsJson(result
? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "We could not delete all of the requests. Please try again or check the logs." });
@ -379,9 +409,9 @@ namespace PlexRequests.UI.Modules
}
}
private Response UpdateRequests(RequestedModel[] requestedModels)
private async Task<Response> UpdateRequestsAsync(RequestedModel[] requestedModels)
{
var cpSettings = CpService.GetSettings();
var cpSettings = await CpService.GetSettingsAsync();
var updatedRequests = new List<RequestedModel>();
foreach (var r in requestedModels)
{
@ -409,8 +439,8 @@ namespace PlexRequests.UI.Modules
if (r.Type == RequestType.TvShow)
{
var sender = new TvSender(SonarrApi, SickRageApi);
var sr = SickRageSettings.GetSettings();
var sonarr = SonarrSettings.GetSettings();
var sr = await SickRageSettings.GetSettingsAsync();
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sr.Enabled)
{
var res = sender.SendToSickRage(sr, r);
@ -449,7 +479,7 @@ namespace PlexRequests.UI.Modules
}
try
{
var result = Service.BatchUpdate(updatedRequests);
var result = await Service.BatchUpdateAsync(updatedRequests);
return Response.AsJson(result
? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "We could not approve all of the requests. Please try again or check the logs." });

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

Loading…
Cancel
Save