Merge pull request #618 from tidusjar/dev

Release 1.9.5
pull/624/head^2 v1.9.5
Jamie 8 years ago committed by GitHub
commit ea2f4d3e78

@ -44,6 +44,6 @@ namespace PlexRequests.Api.Interfaces
PlexSearch GetAllEpisodes(string authToken, Uri host, string section, int startPage, int returnCount);
PlexServer GetServer(string authToken);
PlexSeasonMetadata GetSeasons(string authToken, Uri plexFullHost, string ratingKey);
RecentlyAdded RecentlyAdded(string authToken, Uri plexFullHost);
RecentlyAddedModel RecentlyAdded(string authToken, Uri plexFullHost, string sectionId);
}
}

@ -39,6 +39,10 @@ namespace PlexRequests.Api.Interfaces
int seasonCount, int[] seasons, string apiKey, Uri baseUrl, bool monitor = true,
bool searchForMissingEpisodes = false);
SonarrAddSeries AddSeriesNew(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath,
int[] seasons, string apiKey, Uri baseUrl, bool monitor = true,
bool searchForMissingEpisodes = false);
SystemStatus SystemStatus(string apiKey, Uri baseUrl);
List<Series> GetSeries(string apiKey, Uri baseUrl);

@ -1,63 +1,64 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexAuthentication.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;
namespace PlexRequests.Api.Models.Plex
{
public class PlexAuthentication
{
public User user { get; set; }
}
public class Subscription
{
public bool active { get; set; }
public string status { get; set; }
public object plan { get; set; }
public object features { get; set; }
}
public class Roles
{
public List<object> roles { get; set; }
}
public class User
{
public string email { get; set; }
public string joined_at { get; set; }
public string username { get; set; }
public string title { get; set; }
public string authentication_token { get; set; }
public Subscription subscription { get; set; }
public Roles roles { get; set; }
public List<string> entitlements { get; set; }
public object confirmed_at { get; set; }
public int forum_id { get; set; }
}
}
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexAuthentication.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;
namespace PlexRequests.Api.Models.Plex
{
public class PlexAuthentication
{
public User user { get; set; }
}
public class Subscription
{
public bool active { get; set; }
public string status { get; set; }
public object plan { get; set; }
public object features { get; set; }
}
public class Roles
{
public List<object> roles { get; set; }
}
public class User
{
public string email { get; set; }
public string uuid { get; set; }
public string joined_at { get; set; }
public string username { get; set; }
public string title { get; set; }
public string authentication_token { get; set; }
public Subscription subscription { get; set; }
public Roles roles { get; set; }
public List<string> entitlements { get; set; }
public object confirmed_at { get; set; }
public int forum_id { get; set; }
}
}

@ -1,7 +1,7 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: RecentlyAdded.cs
// File: RecentlyAddedModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -32,53 +32,102 @@ namespace PlexRequests.Api.Models.Plex
public class RecentlyAddedChild
{
public string _elementType { get; set; }
public string allowSync { get; set; }
public string librarySectionID { get; set; }
public string librarySectionTitle { get; set; }
public string librarySectionUUID { get; set; }
public int ratingKey { get; set; }
public string key { get; set; }
public int parentRatingKey { get; set; }
public int grandparentRatingKey { get; set; }
public string type { get; set; }
public string title { get; set; }
public string grandparentKey { get; set; }
public string parentKey { get; set; }
public string parentTitle { get; set; }
public string parentSummary { get; set; }
public string grandparentTitle { get; set; }
public string summary { get; set; }
public int index { get; set; }
public int parentIndex { get; set; }
public string thumb { get; set; }
public string art { get; set; }
public string parentThumb { get; set; }
public int leafCount { get; set; }
public int viewedLeafCount { get; set; }
public string grandparentThumb { get; set; }
public string grandparentArt { get; set; }
public int duration { get; set; }
public int addedAt { get; set; }
public int updatedAt { get; set; }
public List<object> _children { get; set; }
public string studio { get; set; }
public string chapterSource { get; set; }
public List<Child2> _children { get; set; }
public string contentRating { get; set; }
public string rating { get; set; }
public int? viewCount { get; set; }
public int? lastViewedAt { get; set; }
public int? year { get; set; }
public int? duration { get; set; }
public string parentThumb { get; set; }
public string grandparentTheme { get; set; }
public string originallyAvailableAt { get; set; }
public string chapterSource { get; set; }
public string parentTheme { get; set; }
public string titleSort { get; set; }
public string tagline { get; set; }
public int? viewCount { get; set; }
public int? lastViewedAt { get; set; }
public int? viewOffset { get; set; }
public string rating { get; set; }
public string studio { get; set; }
public string tagline { get; set; }
public string originalTitle { get; set; }
public string audienceRating { get; set; }
public string audienceRatingImage { get; set; }
public string ratingImage { get; set; }
}
public class Child3
{
public string _elementType { get; set; }
public string id { get; set; }
public string key { get; set; }
public double duration { get; set; }
public string file { get; set; }
public double size { get; set; }
public string audioProfile { get; set; }
public string container { get; set; }
public string videoProfile { get; set; }
public string deepAnalysisVersion { get; set; }
public string requiredBandwidths { get; set; }
public string hasThumbnail { get; set; }
public bool? has64bitOffsets { get; set; }
public bool? optimizedForStreaming { get; set; }
public bool? hasChapterTextStream { get; set; }
}
public class RecentlyAdded
public class Child2
{
public string _elementType { get; set; }
public string videoResolution { get; set; }
public int id { get; set; }
public int duration { get; set; }
public int bitrate { get; set; }
public int width { get; set; }
public int height { get; set; }
public string aspectRatio { get; set; }
public int audioChannels { get; set; }
public string audioCodec { get; set; }
public string videoCodec { get; set; }
public string container { get; set; }
public string videoFrameRate { get; set; }
public string audioProfile { get; set; }
public string videoProfile { get; set; }
public List<Child3> _children { get; set; }
public string tag { get; set; }
}
public class RecentlyAddedModel
{
public string _elementType { get; set; }
public string allowSync { get; set; }
public string art { get; set; }
public string identifier { get; set; }
public string librarySectionID { get; set; }
public string librarySectionTitle { get; set; }
public string librarySectionUUID { get; set; }
public string mediaTagPrefix { get; set; }
public string mediaTagVersion { get; set; }
public string mixedParents { get; set; }
public string nocache { get; set; }
public string thumb { get; set; }
public string title1 { get; set; }
public string title2 { get; set; }
public string viewGroup { get; set; }
public string viewMode { get; set; }
public List<RecentlyAddedChild> _children { get; set; }
}
}

@ -73,7 +73,7 @@
<Compile Include="Plex\PlexStatus.cs" />
<Compile Include="Plex\PlexMediaType.cs" />
<Compile Include="Plex\PlexUserRequest.cs" />
<Compile Include="Plex\RecentlyAdded.cs" />
<Compile Include="Plex\RecentlyAddedModel.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SickRage\SickRageBase.cs" />
<Compile Include="SickRage\SickrageShows.cs" />

@ -1,79 +1,89 @@
using System.Collections.Generic;
namespace PlexRequests.Api.Models.Tv
{
public class TvMazeShow
{
public int id { get; set; }
public string url { get; set; }
public string name { get; set; }
public string type { get; set; }
public string language { get; set; }
public List<string> genres { get; set; }
public string status { get; set; }
public int runtime { get; set; }
public string premiered { get; set; }
public Schedule schedule { get; set; }
public Rating rating { get; set; }
public int weight { get; set; }
public Network network { get; set; }
public object webChannel { get; set; }
public Externals externals { get; set; }
public Image image { get; set; }
public string summary { get; set; }
public int updated { get; set; }
public Links _links { get; set; }
public int seasonCount { get; set; }
public Embedded _embedded { get; set; }
}
public class Season
{
public int id { get; set; }
public string url { get; set; }
public int number { get; set; }
public string name { get; set; }
public int? episodeOrder { get; set; }
public string premiereDate { get; set; }
public string endDate { get; set; }
public Network2 network { get; set; }
public object webChannel { get; set; }
public Image2 image { get; set; }
public string summary { get; set; }
public Links2 _links { get; set; }
}
public class Country2
{
public string name { get; set; }
public string code { get; set; }
public string timezone { get; set; }
}
public class Network2
{
public int id { get; set; }
public string name { get; set; }
public Country2 country { get; set; }
}
public class Image2
{
public string medium { get; set; }
public string original { get; set; }
}
public class Self2
{
public string href { get; set; }
}
public class Links2
{
public Self2 self { get; set; }
}
public class Embedded
{
public List<Season> seasons { get; set; }
}
using System.Collections.Generic;
namespace PlexRequests.Api.Models.Tv
{
public class TvMazeShow
{
public TvMazeShow()
{
Season = new List<TvMazeCustomSeason>();
}
public int id { get; set; }
public string url { get; set; }
public string name { get; set; }
public string type { get; set; }
public string language { get; set; }
public List<string> genres { get; set; }
public string status { get; set; }
public int runtime { get; set; }
public string premiered { get; set; }
public Schedule schedule { get; set; }
public Rating rating { get; set; }
public int weight { get; set; }
public Network network { get; set; }
public object webChannel { get; set; }
public Externals externals { get; set; }
public Image image { get; set; }
public string summary { get; set; }
public int updated { get; set; }
public Links _links { get; set; }
public List<TvMazeCustomSeason> Season { get; set; }
public Embedded _embedded { get; set; }
}
public class TvMazeCustomSeason
{
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
}
public class Season
{
public int id { get; set; }
public string url { get; set; }
public int number { get; set; }
public string name { get; set; }
public int? episodeOrder { get; set; }
public string premiereDate { get; set; }
public string endDate { get; set; }
public Network2 network { get; set; }
public object webChannel { get; set; }
public Image2 image { get; set; }
public string summary { get; set; }
public Links2 _links { get; set; }
}
public class Country2
{
public string name { get; set; }
public string code { get; set; }
public string timezone { get; set; }
}
public class Network2
{
public int id { get; set; }
public string name { get; set; }
public Country2 country { get; set; }
}
public class Image2
{
public string medium { get; set; }
public string original { get; set; }
}
public class Self2
{
public string href { get; set; }
}
public class Links2
{
public Self2 self { get; set; }
}
public class Embedded
{
public List<Season> seasons { get; set; }
}
}

@ -76,7 +76,7 @@ namespace PlexRequests.Api
Method = Method.POST
};
AddHeaders(ref request);
AddHeaders(ref request, false);
request.AddJsonBody(userModel);
@ -93,7 +93,7 @@ namespace PlexRequests.Api
Method = Method.GET,
};
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
var users = RetryHandler.Execute(() => Api.ExecuteXml<PlexFriends> (request, new Uri(FriendsUri)),
(exception, timespan) => Log.Error (exception, "Exception when calling GetUsers for Plex, Retrying {0}", timespan), null);
@ -118,7 +118,7 @@ namespace PlexRequests.Api
};
request.AddUrlSegment("searchTerm", searchTerm);
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
var search = RetryHandler.Execute(() => Api.ExecuteXml<PlexSearch> (request, plexFullHost),
(exception, timespan) => Log.Error (exception, "Exception when calling SearchContent for Plex, Retrying {0}", timespan), null);
@ -133,7 +133,7 @@ namespace PlexRequests.Api
Method = Method.GET,
};
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
var users = RetryHandler.Execute(() => Api.ExecuteXml<PlexStatus> (request, uri),
(exception, timespan) => Log.Error (exception, "Exception when calling GetStatus for Plex, Retrying {0}", timespan), null);
@ -148,7 +148,7 @@ namespace PlexRequests.Api
Method = Method.GET,
};
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
var account = RetryHandler.Execute(() => Api.ExecuteXml<PlexAccount> (request, new Uri(GetAccountUri)),
(exception, timespan) => Log.Error (exception, "Exception when calling GetAccount for Plex, Retrying {0}", timespan), null);
@ -164,7 +164,7 @@ namespace PlexRequests.Api
Resource = "library/sections"
};
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
try
{
@ -193,7 +193,7 @@ namespace PlexRequests.Api
};
request.AddUrlSegment("libraryId", libraryId);
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
try
{
@ -228,7 +228,7 @@ namespace PlexRequests.Api
};
request.AddUrlSegment("ratingKey", ratingKey);
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
try
{
@ -253,10 +253,9 @@ namespace PlexRequests.Api
};
request.AddQueryParameter("type", 4.ToString());
request.AddQueryParameter("X-Plex-Container-Start", startPage.ToString());
request.AddQueryParameter("X-Plex-Container-Size", returnCount.ToString());
AddLimitHeaders(ref request, startPage, returnCount);
request.AddUrlSegment("section", section);
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
try
{
@ -281,7 +280,7 @@ namespace PlexRequests.Api
};
request.AddUrlSegment("itemId", itemId);
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
try
{
@ -311,7 +310,7 @@ namespace PlexRequests.Api
};
request.AddUrlSegment("ratingKey", ratingKey);
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
try
{
@ -338,7 +337,7 @@ namespace PlexRequests.Api
Method = Method.GET,
};
AddHeaders(ref request, authToken);
AddHeaders(ref request, authToken, false);
var servers = RetryHandler.Execute(() => Api.ExecuteXml<PlexServer>(request, new Uri(ServerUri)),
(exception, timespan) => Log.Error(exception, "Exception when calling GetServer for Plex, Retrying {0}", timespan));
@ -347,25 +346,22 @@ namespace PlexRequests.Api
return servers;
}
public RecentlyAdded RecentlyAdded(string authToken, Uri plexFullHost)
public RecentlyAddedModel RecentlyAdded(string authToken, Uri plexFullHost, string sectionId)
{
var request = new RestRequest
{
Method = Method.GET,
Resource = "library/recentlyAdded"
Resource = "library/sections/{sectionId}/recentlyAdded"
};
request.AddHeader("X-Plex-Token", authToken);
request.AddHeader("X-Plex-Client-Identifier", $"PlexRequests.Net{Version}");
request.AddHeader("X-Plex-Product", "Plex Requests .Net");
request.AddHeader("X-Plex-Version", Version);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddUrlSegment("sectionId", sectionId);
AddHeaders(ref request, authToken, true);
AddLimitHeaders(ref request, 0, 25);
try
{
var lib = RetryHandler.Execute(() => Api.ExecuteJson<RecentlyAdded>(request, plexFullHost),
(exception, timespan) => Log.Error(exception, "Exception when calling RecentlyAdded for Plex, Retrying {0}", timespan), new[] {
var lib = RetryHandler.Execute(() => Api.ExecuteJson<RecentlyAddedModel>(request, plexFullHost),
(exception, timespan) => Log.Error(exception, "Exception when calling RecentlyAddedModel for Plex, Retrying {0}", timespan), new[] {
TimeSpan.FromSeconds (5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
@ -375,23 +371,30 @@ namespace PlexRequests.Api
}
catch (Exception e)
{
Log.Error(e, "There has been a API Exception when attempting to get the Plex RecentlyAdded");
return new RecentlyAdded();
Log.Error(e, "There has been a API Exception when attempting to get the Plex RecentlyAddedModel");
return new RecentlyAddedModel();
}
}
private void AddHeaders(ref RestRequest request, string authToken)
private void AddLimitHeaders(ref RestRequest request, int from, int to)
{
request.AddHeader("X-Plex-Container-Start", from.ToString());
request.AddHeader("X-Plex-Container-Size", to.ToString());
}
private void AddHeaders(ref RestRequest request, string authToken, bool json)
{
request.AddHeader("X-Plex-Token", authToken);
AddHeaders(ref request);
AddHeaders(ref request, json);
}
private void AddHeaders(ref RestRequest request)
private void AddHeaders(ref RestRequest request, bool json)
{
request.AddHeader("X-Plex-Client-Identifier", $"PlexRequests.Net{Version}");
request.AddHeader("X-Plex-Product", "Plex Requests .Net");
request.AddHeader("X-Plex-Version", Version);
request.AddHeader("Content-Type", "application/xml");
request.AddHeader("Content-Type", json ? "application/json" : "application/xml");
request.AddHeader("Accept", json ? "application/json" : "application/xml");
}
}
}

@ -62,8 +62,9 @@
<Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath>
</Reference>
<Reference Include="TMDbLib, Version=0.9.0.0, Culture=neutral, PublicKeyToken=null">
<Reference Include="TMDbLib, Version=0.9.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>

@ -113,6 +113,75 @@ namespace PlexRequests.Api
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(options);
SonarrAddSeries result;
try
{
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling AddSeries for Sonarr, Retrying {0}", timespan), new TimeSpan[] {
TimeSpan.FromSeconds (1),
TimeSpan.FromSeconds(2),
});
result = policy.Execute(() => Api.ExecuteJson<SonarrAddSeries>(request, baseUrl));
}
catch (JsonSerializationException jse)
{
Log.Error(jse);
var error = Api.ExecuteJson<List<SonarrError>>(request, baseUrl);
var messages = error?.Select(x => x.errorMessage).ToList();
messages?.ForEach(x => Log.Error(x));
result = new SonarrAddSeries { ErrorMessages = messages };
}
return result;
}
public SonarrAddSeries AddSeriesNew(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int[] seasons, string apiKey, Uri baseUrl, bool monitor = true, bool searchForMissingEpisodes = false)
{
var request = new RestRequest
{
Resource = "/api/Series?",
Method = Method.POST
};
var options = new SonarrAddSeries
{
seasonFolder = seasonFolders,
title = title,
qualityProfileId = qualityId,
tvdbId = tvdbId,
titleSlug = title,
seasons = new List<Season>(),
rootFolderPath = rootPath,
monitored = monitor
};
if (!searchForMissingEpisodes)
{
options.addOptions = new AddOptions
{
searchForMissingEpisodes = false,
ignoreEpisodesWithFiles = true,
ignoreEpisodesWithoutFiles = true
};
}
for (var i = 1; i <= seasons.Length; i++)
{
var season = new Season
{
seasonNumber = i,
// ReSharper disable once SimplifyConditionalTernaryExpression
monitored = true
};
options.seasons.Add(season);
}
Log.Debug("Sonarr API Options:");
Log.Debug(options.DumpJson());
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(options);
SonarrAddSeries result;
try
{

@ -93,8 +93,18 @@ namespace PlexRequests.Api
request.AddHeader("Content-Type", "application/json");
var obj = Api.Execute<TvMazeShow>(request, new Uri(Uri));
obj.seasonCount = GetSeasonCount(obj.id);
var episodes = EpisodeLookup(obj.id).ToList();
foreach (var e in episodes)
{
obj.Season.Add(new TvMazeCustomSeason
{
SeasonNumber = e.season,
EpisodeNumber = e.number
});
}
return obj;
}
@ -110,6 +120,7 @@ namespace PlexRequests.Api
return Api.Execute<List<TvMazeSeasons>>(request, new Uri(Uri));
}
public int GetSeasonCount(int id)
{
var obj = GetSeasons(id);

@ -6,5 +6,6 @@
<package id="NLog" version="4.3.6" targetFramework="net45" />
<package id="Polly-Signed" version="4.3.0" targetFramework="net45" />
<package id="RestSharp" version="105.2.3" targetFramework="net45" />
<package id="System.Net.Http" version="4.0.0" targetFramework="net45" />
<package id="TMDbLib" version="0.9.0.0-alpha" targetFramework="net45" />
</packages>

@ -0,0 +1,37 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: IMigration.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.Data;
namespace PlexRequests.Core.Migration
{
public interface IMigration
{
int Version { get; }
void Start(IDbConnection con);
}
}

@ -0,0 +1,7 @@
namespace PlexRequests.Core.Migration
{
public interface IMigrationRunner
{
void MigrateToLatest();
}
}

@ -0,0 +1,33 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: Migrate.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.Migration
{
public class Migrate
{
}
}

@ -0,0 +1,43 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: Migration.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.Core.Migration
{
[AttributeUsage(AttributeTargets.Class)]
public class Migration : Attribute
{
public Migration(int version, string description)
{
Version = version;
Description = description;
}
public int Version { get; private set; }
public string Description { get; private set; }
}
}

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Reflection;
using Ninject;
using PlexRequests.Store;
namespace PlexRequests.Core.Migration
{
public class MigrationRunner : IMigrationRunner
{
public MigrationRunner(ISqliteConfiguration db, IKernel kernel)
{
Db = db;
Kernel = kernel;
}
private IKernel Kernel { get; }
private ISqliteConfiguration Db { get; }
public void MigrateToLatest()
{
var con = Db.DbConnection();
var versions = GetMigrations().OrderBy(x => x.Key);
var dbVersion = con.GetVersionInfo().OrderByDescending(x => x.Version).FirstOrDefault();
if (dbVersion == null)
{
dbVersion = new TableCreation.VersionInfo { Version = 0 };
}
foreach (var v in versions)
{
if (v.Value.Version > dbVersion.Version)
{
// Assuming only one constructor
var ctor = v.Key.GetConstructors().FirstOrDefault();
var dependencies = new List<object>();
foreach (var param in ctor.GetParameters())
{
Console.WriteLine(string.Format(
"Param {0} is named {1} and is of type {2}",
param.Position, param.Name, param.ParameterType));
var dep = Kernel.Get(param.ParameterType);
dependencies.Add(dep);
}
var method = v.Key.GetMethod("Start");
if (method != null)
{
object result = null;
var classInstance = Activator.CreateInstance(v.Key, dependencies.Any() ? dependencies.ToArray() : null);
var parametersArray = new object[] { Db.DbConnection() };
method.Invoke(classInstance, parametersArray);
}
}
}
}
public static Dictionary<Type, MigrationModel> GetMigrations()
{
var migrationTypes = GetTypesWithHelpAttribute(Assembly.GetAssembly(typeof(MigrationRunner)));
var version = new Dictionary<Type, MigrationModel>();
foreach (var t in migrationTypes)
{
var customAttributes = (Migration[])t.GetCustomAttributes(typeof(Migration), true);
if (customAttributes.Length > 0)
{
var attr = customAttributes[0];
version.Add(t, new MigrationModel { Version = attr.Version, Description = attr.Description });
}
}
return version;
}
private static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly)
{
return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(Migration), true).Length > 0);
}
public class MigrationModel
{
public int Version { get; set; }
public string Description { get; set; }
}
}
}

@ -0,0 +1,53 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: BaseMigration.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.Data;
using System.Linq;
using PlexRequests.Store;
namespace PlexRequests.Core.Migration.Migrations
{
public abstract class BaseMigration
{
protected void UpdateSchema(IDbConnection con, int version)
{
var migrations = MigrationRunner.GetMigrations();
var model = migrations.Select(x => x.Value).FirstOrDefault(x => x.Version == version);
if (model != null)
{
con.AddVersionInfo(new TableCreation.VersionInfo { Version = model.Version, Description = model.Description });
}
}
protected IEnumerable<TableCreation.VersionInfo> GetVersionInfo(IDbConnection con)
{
return con.GetVersionInfo();
}
}
}

@ -0,0 +1,99 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: Version195.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.Data;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using Quartz;
namespace PlexRequests.Core.Migration.Migrations
{
[Migration(1950, "v1.9.5.0")]
public class Version195 : BaseMigration, IMigration
{
public Version195(ISettingsService<PlexRequestSettings> plexRequestSettings, ISettingsService<NewletterSettings> news, ISettingsService<ScheduledJobsSettings> jobs)
{
PlexRequestSettings = plexRequestSettings;
NewsletterSettings = news;
Jobs = jobs;
}
public int Version => 1950;
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private ISettingsService<NewletterSettings> NewsletterSettings { get; }
private ISettingsService<ScheduledJobsSettings> Jobs { get; }
public void Start(IDbConnection con)
{
UpdateApplicationSettings();
UpdateDb(con);
UpdateSchema(con, Version);
}
private void UpdateDb(IDbConnection con)
{
}
private void UpdateApplicationSettings()
{
var plex = PlexRequestSettings.GetSettings();
var jobSettings = Jobs.GetSettings();
var newsLetter = NewsletterSettings.GetSettings();
newsLetter.SendToPlexUsers = true;
UpdateScheduledSettings(jobSettings);
if (plex.SendRecentlyAddedEmail)
{
newsLetter.SendRecentlyAddedEmail = plex.SendRecentlyAddedEmail;
plex.SendRecentlyAddedEmail = false;
PlexRequestSettings.SaveSettings(plex);
}
NewsletterSettings.SaveSettings(newsLetter);
Jobs.SaveSettings(jobSettings);
}
private void UpdateScheduledSettings(ScheduledJobsSettings settings)
{
settings.PlexAvailabilityChecker = 60;
settings.SickRageCacher = 60;
settings.SonarrCacher = 60;
settings.CouchPotatoCacher = 60;
settings.StoreBackup = 24;
settings.StoreCleanup = 24;
settings.UserRequestLimitResetter = 12;
settings.PlexEpisodeCacher = 12;
var cron = (Quartz.Impl.Triggers.CronTriggerImpl)CronScheduleBuilder.WeeklyOnDayAndHourAndMinute(DayOfWeek.Friday, 7, 0).Build();
settings.RecentlyAddedCron = cron.CronExpressionString; // Weekly CRON at 7 am on Mondays
}
}
}

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8406EE57-D533-47C0-9302-C6B5F8C31E55}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PlexRequests.Core.Migration</RootNamespace>
<AssemblyName>PlexRequests.Core.Migration</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<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="Mono.Data.Sqlite">
<HintPath>..\Assemblies\Mono.Data.Sqlite.dll</HintPath>
</Reference>
<Reference Include="Ninject">
<HintPath>..\packages\Ninject.3.2.0.0\lib\net45-full\Ninject.dll</HintPath>
</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" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="IMigration.cs" />
<Compile Include="IMigrationRunner.cs" />
<Compile Include="Migrate.cs" />
<Compile Include="MigrationAttribute.cs" />
<Compile Include="MigrationRunner.cs" />
<Compile Include="Migrations\BaseMigration.cs" />
<Compile Include="Migrations\Version195.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PlexRequests.Core\PlexRequests.Core.csproj">
<Project>{DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}</Project>
<Name>PlexRequests.Core</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Store\PlexRequests.Store.csproj">
<Project>{92433867-2B7B-477B-A566-96C382427525}</Project>
<Name>PlexRequests.Store</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="job_scheduling_data_2_0.xsd">
<SubType>Designer</SubType>
</None>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("PlexRequests.Core.Migration")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("PlexRequests.Core.Migration")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("8406ee57-d533-47c0-9302-c6b5f8c31e55")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

@ -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-9.0.0.0" newVersion="9.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

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

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Common.Logging" version="3.0.0" targetFramework="net45" />
<package id="Common.Logging.Core" version="3.0.0" targetFramework="net45" />
<package id="Quartz" version="2.3.3" targetFramework="net45" />
</packages>

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using PlexRequests.Store.Models.Plex;
namespace PlexRequests.Core
{
public interface IPlexReadOnlyDatabase
{
IEnumerable<MetadataItems> GetItemsAddedAfterDate(DateTime dateTime);
}
}

@ -144,7 +144,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr>
<td align="center">
<img src="http://i.imgur.com/s4nswSA.png?" width="400px" text-align="center" />
<img src="http://i.imgur.com/ROTp8mn.png" text-align="center" />
</td>
</tr>
<tr>

@ -0,0 +1,82 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexReadOnlyDatabase.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 PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using PlexRequests.Store.Models.Plex;
namespace PlexRequests.Core
{
public class PlexReadOnlyDatabase : IPlexReadOnlyDatabase
{
public PlexReadOnlyDatabase(IPlexDatabase plexDatabase, ISettingsService<PlexSettings> plexSettings)
{
Plex = plexDatabase;
var settings = plexSettings.GetSettings();
if (!string.IsNullOrEmpty(settings.PlexDatabaseLocationOverride))
{
//Overriden setting
Plex.DbLocation = Path.Combine(settings.PlexDatabaseLocationOverride, "Plug-in Support", "Databases", "com.plexapp.plugins.library.db");
}
else if (Type.GetType("Mono.Runtime") != null)
{
// Mono
Plex.DbLocation = Path.Combine("/var/lib/plexmediaserver/Library/Application Support/", "Plex Media Server", "Plug-in Support", "Databases", "com.plexapp.plugins.library.db");
}
else
{
// Default Windows
Plex.DbLocation = Path.Combine(Environment.ExpandEnvironmentVariables("%LOCALAPPDATA%"), "Plex Media Server", "Plug-in Support", "Databases", "com.plexapp.plugins.library.db");
}
}
private IPlexDatabase Plex { get; }
public IEnumerable<MetadataItems> GetItemsAddedAfterDate(DateTime dateTime)
{
// type 1 = Movie, type 4 = TV Episode
var movies = Plex.QueryMetadataItems(@"SELECT * FROM metadata_items
WHERE added_at > @AddedAt
AND metadata_type = 1
AND title <> ''", new { AddedAt = dateTime });
// Custom query to include the series title
var tv = Plex.QueryMetadataItems(@"SELECT series.title AS SeriesTitle, mi.* FROM metadata_items mi
INNER JOIN metadata_items season ON mi.parent_id = season.id
INNER JOIN metadata_items series ON series.id = season.parent_id
WHERE mi.added_at > @AddedAt
AND mi.metadata_type = 4", new { AddedAt = dateTime });
return movies.Union(tv);
}
}
}

@ -69,6 +69,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="CacheKeys.cs" />
<Compile Include="IPlexReadOnlyDatabase.cs" />
<Compile Include="Notification\NotificationMessage.cs" />
<Compile Include="Notification\NotificationMessageContent.cs" />
<Compile Include="Notification\NotificationMessageCurlys.cs" />
@ -85,10 +86,12 @@
<Compile Include="Notification\Templates\EmailBasicTemplate.cs" />
<Compile Include="Notification\Templates\IEmailBasicTemplate.cs" />
<Compile Include="Notification\TransportType.cs" />
<Compile Include="PlexReadOnlyDatabase.cs" />
<Compile Include="SettingModels\AuthenticationSettings.cs" />
<Compile Include="SettingModels\ExternalSettings.cs" />
<Compile Include="SettingModels\HeadphonesSettings.cs" />
<Compile Include="SettingModels\LandingPageSettings.cs" />
<Compile Include="SettingModels\NewsletterSettings.cs" />
<Compile Include="SettingModels\NotificationSettings.cs" />
<Compile Include="SettingModels\NotificationSettingsV2.cs" />
<Compile Include="SettingModels\RequestSettings.cs" />
@ -141,7 +144,7 @@
</ItemGroup>
<ItemGroup>
<Content Include="Notification\Templates\BasicRequestTemplate.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

@ -0,0 +1,45 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: NewsletterSettings.cs
// Created By: Jim MacKenzie
//
// 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 Newtonsoft.Json;
using PlexRequests.Helpers;
namespace PlexRequests.Core.SettingModels
{
public class NewletterSettings : Settings
{
public bool SendRecentlyAddedEmail { get; set; }
public bool SendToPlexUsers { get; set; }
public string CustomUsers { get; set; }
[JsonIgnore]
public IEnumerable<string> CustomUsersEmailAddresses => CustomUsers.SplitEmailsByDelimiter(';');
}
}

@ -58,8 +58,8 @@ namespace PlexRequests.Core.SettingModels
public bool Wizard { get; set; }
public bool DisableTvRequestsByEpisode { get; set; }
public bool DisableTvRequestsBySeason { get; set; }
[Obsolete("Moved to NewsLetterSettings")]
public bool SendRecentlyAddedEmail { get; set; }
public string CustomDonationUrl { get; set; }
public bool EnableCustomDonationUrl { get; set; }
public string CustomDonationMessage { get; set; }

@ -40,5 +40,6 @@ namespace PlexRequests.Core.SettingModels
public string PlexAuthToken { get; set; }
public string MachineIdentifier { get; set; }
public string PlexDatabaseLocationOverride { get; set; }
}
}

@ -24,23 +24,13 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
namespace PlexRequests.Core.SettingModels
{
public class ScheduledJobsSettings : Settings
{
public ScheduledJobsSettings()
{
PlexAvailabilityChecker = 60;
SickRageCacher = 60;
SonarrCacher = 60;
CouchPotatoCacher = 60;
StoreBackup = 24;
StoreCleanup = 24;
UserRequestLimitResetter = 12;
PlexEpisodeCacher = 12;
RecentlyAdded = 168;
}
public int PlexAvailabilityChecker { get; set; }
public int SickRageCacher { get; set; }
public int SonarrCacher { get; set; }
@ -49,6 +39,8 @@ namespace PlexRequests.Core.SettingModels
public int StoreCleanup { get; set; }
public int UserRequestLimitResetter { get; set; }
public int PlexEpisodeCacher { get; set; }
[Obsolete("We use the CRON job now")]
public int RecentlyAdded { get; set; }
public string RecentlyAddedCron { get; set; }
}
}

@ -60,6 +60,8 @@ namespace PlexRequests.Core
TableCreation.Vacuum(Db.DbConnection());
}
// The below code is obsolete, we should use PlexRequests.Core.Migrations.MigrationRunner
var version = CheckSchema();
if (version > 0)
{

@ -110,31 +110,35 @@ namespace PlexRequests.Core
Salt = salt,
Hash = PasswordHasher.ComputeHash(password, salt),
Claims = ByteConverterHelper.ReturnBytes(claims),
UserProperties = ByteConverterHelper.ReturnBytes(properties ?? new UserProperties())
UserProperties = ByteConverterHelper.ReturnBytes(properties ?? new UserProperties()),
};
Repo.Insert(userModel);
var userRecord = Repo.Get(userModel.UserGuid);
return new Guid(userRecord.UserGuid);
}
public void DeleteUser(string userId)
{
var user = Repo.Get(userId);
Repo.Delete(user);
}
public Guid? CreateAdmin(string username, string password, UserProperties properties = null)
{
return CreateUser(username, password, new[] { UserClaims.User, UserClaims.PowerUser, UserClaims.Admin }, properties);
return CreateUser(username, password, new[] { UserClaims.RegularUser, UserClaims.PowerUser, UserClaims.Admin }, properties);
}
public Guid? CreatePowerUser(string username, string password, UserProperties properties = null)
{
return CreateUser(username, password, new[] { UserClaims.User, UserClaims.PowerUser }, properties);
return CreateUser(username, password, new[] { UserClaims.RegularUser, UserClaims.PowerUser }, properties);
}
public Guid? CreateRegularUser(string username, string password, UserProperties properties = null)
{
return CreateUser(username, password, new[] { UserClaims.User }, properties);
return CreateUser(username, password, new[] { UserClaims.RegularUser }, properties);
}
public IEnumerable<string> GetAllClaims()
{
var properties = typeof(UserClaims).GetConstantsValues<string>();
@ -194,7 +198,6 @@ namespace PlexRequests.Core
Guid? CreateAdmin(string username, string password, UserProperties properties = null);
Guid? CreatePowerUser(string username, string password, UserProperties properties = null);
Guid? CreateRegularUser(string username, string password, UserProperties properties = null);
void DeleteUser(string userId);
}
}

@ -1,42 +1,50 @@
using System;
using System.Globalization;
using System.Linq;
namespace PlexRequests.Helpers
{
public static class DateTimeHelper
{
public static DateTimeOffset OffsetUTCDateTime(DateTime utcDateTime, int minuteOffset)
{
//TimeSpan ts = TimeSpan.FromMinutes(-minuteOffset);
//return new DateTimeOffset(utcDateTime).ToOffset(ts);
// this is a workaround below to work with MONO
var tzi = FindTimeZoneFromOffset(minuteOffset);
var utcOffset = tzi.GetUtcOffset(utcDateTime);
var newDate = utcDateTime + utcOffset;
return new DateTimeOffset(newDate.Ticks, utcOffset);
}
public static void CustomParse(string date, out DateTime dt)
{
// Try and parse it
if (DateTime.TryParse(date, out dt))
{
return;
}
// Maybe it's only a year?
if (DateTime.TryParseExact(date, "yyyy", CultureInfo.CurrentCulture, DateTimeStyles.None, out dt))
{
return;
}
}
private static TimeZoneInfo FindTimeZoneFromOffset(int minuteOffset)
{
var tzc = TimeZoneInfo.GetSystemTimeZones();
return tzc.FirstOrDefault(x => x.BaseUtcOffset.TotalMinutes == -minuteOffset);
}
}
}
using System;
using System.Globalization;
using System.Linq;
namespace PlexRequests.Helpers
{
public static class DateTimeHelper
{
public static DateTimeOffset OffsetUTCDateTime(DateTime utcDateTime, int minuteOffset)
{
//TimeSpan ts = TimeSpan.FromMinutes(-minuteOffset);
//return new DateTimeOffset(utcDateTime).ToOffset(ts);
// this is a workaround below to work with MONO
var tzi = FindTimeZoneFromOffset(minuteOffset);
var utcOffset = tzi.GetUtcOffset(utcDateTime);
var newDate = utcDateTime + utcOffset;
return new DateTimeOffset(newDate.Ticks, utcOffset);
}
public static void CustomParse(string date, out DateTime dt)
{
// Try and parse it
if (DateTime.TryParse(date, out dt))
{
return;
}
// Maybe it's only a year?
if (DateTime.TryParseExact(date, "yyyy", CultureInfo.CurrentCulture, DateTimeStyles.None, out dt))
{
return;
}
}
private static TimeZoneInfo FindTimeZoneFromOffset(int minuteOffset)
{
var tzc = TimeZoneInfo.GetSystemTimeZones();
return tzc.FirstOrDefault(x => x.BaseUtcOffset.TotalMinutes == -minuteOffset);
}
public static DateTime UnixTimeStampToDateTime(this int unixTimeStamp)
{
// Unix timestamp is seconds past epoch
System.DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);
dtDateTime = dtDateTime.AddSeconds(unixTimeStamp).ToLocalTime();
return dtDateTime;
}
}
}

@ -102,6 +102,12 @@ namespace PlexRequests.Helpers
$"https://app.plex.tv/web/app#!/server/{machineId}/details/%2Flibrary%2Fmetadata%2F{mediaId}";
return url;
}
public static string FormatGenres(string tags)
{
var split = tags.Split(new[] {'|'}, StringSplitOptions.RemoveEmptyEntries);
return string.Join(", ", split);
}
}
public class EpisodeModelHelper

@ -87,6 +87,7 @@
<Compile Include="TypeHelper.cs" />
<Compile Include="UriHelper.cs" />
<Compile Include="UserClaims.cs" />
<Compile Include="UserType.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />

@ -24,6 +24,8 @@
// 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.Text.RegularExpressions;
@ -64,5 +66,36 @@ namespace PlexRequests.Helpers
}
return sb.ToString();
}
public static IEnumerable<string> SplitEmailsByDelimiter(this string input, char delimiter)
{
if (string.IsNullOrEmpty(input))
{
yield return string.Empty;
}
var startIndex = 0;
var delimiterIndex = 0;
while (delimiterIndex >= 0)
{
delimiterIndex = input.IndexOf(delimiter, startIndex);
string substring = input;
if (delimiterIndex > 0)
{
substring = input.Substring(0, delimiterIndex).Trim();
}
if (!substring.Contains("\"") || substring.IndexOf("\"") != substring.LastIndexOf("\""))
{
yield return substring;
input = input.Substring(delimiterIndex + 1).Trim();
startIndex = 0;
}
else
{
startIndex = delimiterIndex + 1;
}
}
}
}
}

@ -4,9 +4,11 @@ namespace PlexRequests.Helpers
{
public class UserClaims
{
public const string Admin = "Admin"; // Can do everything including creating new users and editing settings
public const string PowerUser = "PowerUser"; // Can only manage the requests, approve etc.
public const string User = "User"; // Can only request
public const string Admin = nameof(Admin); // Can do everything including creating new users and editing settings
public const string PowerUser = nameof(PowerUser); // Can only manage the requests, approve etc.
public const string RegularUser = nameof(RegularUser); // Can only request
public const string ReadOnlyUser = nameof(ReadOnlyUser); // Can only view stuff
public const string Newsletter = nameof(Newsletter); // Has newsletter feature enabled
}
}

@ -0,0 +1,35 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UserType.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
{
public enum UserType
{
PlexUser,
LocalUser
}
}

@ -0,0 +1,71 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: HtmlTemplateGenerator.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.IO;
using System.Text;
using System.Web.UI;
namespace PlexRequests.Services.Jobs
{
public abstract class HtmlTemplateGenerator
{
protected virtual void AddParagraph(StringBuilder stringBuilder, string text, int fontSize = 14, string fontWeight = "normal")
{
stringBuilder.AppendFormat("<p style=\"font-family: sans-serif; font-size: {1}px; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{0}</p>", text, fontSize, fontWeight);
}
protected virtual void AddImageInsideTable(StringBuilder sb, string url)
{
sb.Append("<tr>");
sb.Append("<td align=\"center\">");
sb.AppendFormat(
"<img src=\"{0}\" width=\"400px\" text-align=\"center\" />",
url);
sb.Append("</td>");
sb.Append("</tr>");
}
protected virtual void Href(StringBuilder sb, string url)
{
sb.AppendFormat("<a href=\"{0}\">", url);
}
protected virtual void EndTag(StringBuilder sb, string tag)
{
sb.AppendFormat("</{0}>", tag);
}
protected virtual void Header(StringBuilder sb, int size, string text, string fontWeight = "normal")
{
sb.AppendFormat(
"<h{0} style=\"font-family: sans-serif; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{1}</h{0}>",
size, text, fontWeight);
}
}
}

@ -123,12 +123,13 @@ namespace PlexRequests.Services.Jobs
case RequestType.TvShow:
if (!plexSettings.EnableTvEpisodeSearching)
{
matchResult = IsTvShowAvailable(shows, r.Title, releaseDate, r.TvDbId);
matchResult = IsTvShowAvailable(shows, r.Title, releaseDate, r.TvDbId, r.SeasonList);
}
else
{
matchResult =
r.Episodes.All(x => IsEpisodeAvailable(r.TvDbId, x.SeasonNumber, x.EpisodeNumber));
matchResult = r.Episodes.Any() ?
r.Episodes.All(x => IsEpisodeAvailable(r.TvDbId, x.SeasonNumber, x.EpisodeNumber)) :
IsTvShowAvailable(shows, r.Title, releaseDate, r.TvDbId, r.SeasonList);
}
break;
case RequestType.Album:
@ -270,7 +271,7 @@ namespace PlexRequests.Services.Jobs
{
if (advanced)
{
if (seasons != null && show.ProviderId == providerId)
if (show.ProviderId == providerId && seasons != null)
{
if (seasons.Any(season => show.Seasons.Contains(season)))
{
@ -411,6 +412,8 @@ namespace PlexRequests.Services.Jobs
try
{
// TODO what the fuck was I thinking
if (setCache)
{
results = GetLibraries(plexSettings);

@ -112,6 +112,10 @@ namespace PlexRequests.Services.Jobs
// Loop through the metadata and create the model to insert into the DB
foreach (var metadataVideo in metadata.Video)
{
if(string.IsNullOrEmpty(metadataVideo.GrandparentTitle))
{
continue;
}
var epInfo = PlexHelper.GetSeasonsAndEpisodesFromPlexGuid(metadataVideo.Guid);
entities.TryAdd(
new PlexEpisodes

@ -1,7 +1,8 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: RecentlyAdded.cs
// File: RecentlyAddedModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
@ -23,6 +24,7 @@
// 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;
@ -40,32 +42,37 @@ using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs.Templates;
using PlexRequests.Store.Models.Plex;
using Quartz;
namespace PlexRequests.Services.Jobs
{
public class RecentlyAdded : IJob, IRecentlyAdded
public class RecentlyAdded : HtmlTemplateGenerator, IJob, IRecentlyAdded
{
public RecentlyAdded(IPlexApi api, ISettingsService<PlexSettings> plexSettings, ISettingsService<EmailNotificationSettings> email,
ISettingsService<ScheduledJobsSettings> scheduledService, IJobRecord rec, ISettingsService<PlexRequestSettings> plexRequest)
public RecentlyAdded(IPlexApi api, ISettingsService<PlexSettings> plexSettings,
ISettingsService<EmailNotificationSettings> email, IJobRecord rec,
ISettingsService<NewletterSettings> newsletter,
IPlexReadOnlyDatabase db)
{
JobRecord = rec;
Api = api;
PlexSettings = plexSettings;
EmailSettings = email;
ScheduledJobsSettings = scheduledService;
PlexRequestSettings = plexRequest;
NewsletterSettings = newsletter;
PlexDb = db;
}
private IPlexApi Api { get; }
private TvMazeApi TvApi = new TvMazeApi();
private readonly TheMovieDbApi _movieApi = new TheMovieDbApi();
private const int MetadataTypeTv = 4;
private const int MetadataTypeMovie = 1;
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<EmailNotificationSettings> EmailSettings { get; }
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private ISettingsService<ScheduledJobsSettings> ScheduledJobsSettings { get; }
private ISettingsService<NewletterSettings> NewsletterSettings { get; }
private IJobRecord JobRecord { get; }
private IPlexReadOnlyDatabase PlexDb { get; }
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@ -73,24 +80,13 @@ namespace PlexRequests.Services.Jobs
{
try
{
var settings = PlexRequestSettings.GetSettings();
var settings = NewsletterSettings.GetSettings();
if (!settings.SendRecentlyAddedEmail)
{
return;
}
var jobs = JobRecord.GetJobs();
var thisJob =
jobs.FirstOrDefault(
x => x.Name.Equals(JobNames.RecentlyAddedEmail, StringComparison.CurrentCultureIgnoreCase));
var jobSettings = ScheduledJobsSettings.GetSettings();
if (thisJob?.LastRun > DateTime.Now.AddHours(-jobSettings.RecentlyAdded))
{
return;
}
Start();
Start(settings);
}
catch (Exception e)
{
@ -104,115 +100,143 @@ namespace PlexRequests.Services.Jobs
public void Test()
{
Start(true);
var settings = NewsletterSettings.GetSettings();
Start(settings, true);
}
private void Start(bool testEmail = false)
private void Start(NewletterSettings newletterSettings, bool testEmail = false)
{
var sb = new StringBuilder();
var plexSettings = PlexSettings.GetSettings();
var recentlyAdded = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri);
var libs = Api.GetLibrarySections(plexSettings.PlexAuthToken, plexSettings.FullUri);
var tvSection = libs.Directories.FirstOrDefault(x => x.type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase));
var movieSection = libs.Directories.FirstOrDefault(x => x.type.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase));
var movies =
recentlyAdded._children.Where(x => x.type.Equals("Movie", StringComparison.CurrentCultureIgnoreCase));
var tv =
recentlyAdded._children.Where(
x => x.type.Equals("season", StringComparison.CurrentCultureIgnoreCase))
.GroupBy(x => x.parentTitle)
.Select(x => x.FirstOrDefault());
var recentlyAddedTv = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, tvSection.Key);
var recentlyAddedMovies = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri, movieSection.Key);
GenerateMovieHtml(movies, plexSettings, ref sb);
GenerateTvHtml(tv, plexSettings, ref sb);
GenerateMovieHtml(recentlyAddedMovies, plexSettings, sb);
GenerateTvHtml(recentlyAddedTv, plexSettings, sb);
var template = new RecentlyAddedTemplate();
var html = template.LoadTemplate(sb.ToString());
Send(html, plexSettings, testEmail);
Send(newletterSettings, html, plexSettings, testEmail);
}
private void GenerateMovieHtml(IEnumerable<RecentlyAddedChild> movies, PlexSettings plexSettings, ref StringBuilder sb)
private void GenerateMovieHtml(RecentlyAddedModel movies, PlexSettings plexSettings, StringBuilder sb)
{
sb.Append("<h1>New Movies:</h1><br/><br/>");
sb.Append("<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var movie in movies)
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var movie in movies._children.OrderByDescending(x => x.addedAt.UnixTimeStampToDateTime()))
{
var metaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
movie.ratingKey.ToString());
var plexGUID = string.Empty;
try
{
var metaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
movie.ratingKey.ToString());
var imdbId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Video.Guid);
var info = _movieApi.GetMovieInformation(imdbId).Result;
plexGUID = metaData.Video.Guid;
sb.Append("<tr>");
sb.Append("<td align=\"center\">");
sb.AppendFormat("<img src=\"https://image.tmdb.org/t/p/w500{0}\" width=\"400px\" text-align=\"center\" />", info.BackdropPath);
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("<tr>");
sb.Append("<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
var imdbId = PlexHelper.GetProviderIdFromPlexGuid(plexGUID);
var info = _movieApi.GetMovieInformation(imdbId).Result;
sb.AppendFormat("<a href=\"https://www.imdb.com/title/{0}/\"><h3 style=\"font-family: sans-serif; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{1} {2}</p></a>",
info.ImdbId, info.Title, info.ReleaseDate?.ToString("yyyy") ?? string.Empty);
AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/w500{info.BackdropPath}");
if (info.Genres.Any())
sb.Append("<tr>");
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/");
Header(sb, 3, $"{info.Title} {info.ReleaseDate?.ToString("yyyy") ?? string.Empty}");
EndTag(sb, "a");
if (info.Genres.Any())
{
AddParagraph(sb,
$"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}");
}
AddParagraph(sb, info.Overview);
}
catch (Exception e)
{
sb.AppendFormat(
"<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">Genre: {0}</p>",
string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray()));
Log.Error(e);
Log.Error(
"Exception when trying to process a Movie, either in getting the metadata from Plex OR getting the information from TheMovieDB, Plex GUID = {0}",
plexGUID);
}
finally
{
EndLoopHtml(sb);
}
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{0}</p>", info.Overview);
sb.Append("<td");
sb.Append("<hr>");
sb.Append("<br>");
sb.Append("<br>");
sb.Append("</tr>");
}
sb.Append("</table><br/><br/>");
}
private void GenerateTvHtml(IEnumerable<RecentlyAddedChild> tv, PlexSettings plexSettings, ref StringBuilder sb)
private void GenerateTvHtml(RecentlyAddedModel tv, PlexSettings plexSettings, StringBuilder sb)
{
// TV
sb.Append("<h1>New Episodes:</h1><br/><br/>");
sb.Append("<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var t in tv)
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var t in tv._children.OrderByDescending(x => x.addedAt.UnixTimeStampToDateTime()))
{
var parentMetaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
t.parentRatingKey.ToString());
var plexGUID = string.Empty;
try
{
var parentMetaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
t.parentRatingKey.ToString());
plexGUID = parentMetaData.Directory.Guid;
var info = TvApi.ShowLookupByTheTvDbId(int.Parse(PlexHelper.GetProviderIdFromPlexGuid(plexGUID)));
var info = TvApi.ShowLookupByTheTvDbId(int.Parse(PlexHelper.GetProviderIdFromPlexGuid(parentMetaData.Directory.Guid)));
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
}
AddImageInsideTable(sb, banner);
sb.Append("<tr>");
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
var title = $"{t.grandparentTitle} - {t.title} {t.originallyAvailableAt?.Substring(0, 4)}";
Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/");
Header(sb, 3, title);
EndTag(sb, "a");
AddParagraph(sb, $"Season: {t.parentIndex}, Episode: {t.index}");
if (info.genres.Any())
{
AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}");
}
AddParagraph(sb, string.IsNullOrEmpty(t.summary) ? info.summary : t.summary);
}
catch (Exception e)
{
Log.Error(e);
Log.Error(
"Exception when trying to process a TV Show, either in getting the metadata from Plex OR getting the information from TVMaze, Plex GUID = {0}",
plexGUID);
}
finally
{
banner = banner.Replace("http", "https"); // Always use the Https banners
EndLoopHtml(sb);
}
sb.Append("<tr>");
sb.Append("<td align=\"center\">");
sb.AppendFormat("<img src=\"{0}\" width=\"400px\" text-align=\"center\" />", banner);
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("<tr>");
sb.Append("<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
sb.AppendFormat("<a href=\"https://www.imdb.com/title/{0}/\"><h3 style=\"font-family: sans-serif; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{1} {2}</p></a>",
info.externals.imdb, info.name, info.premiered.Substring(0, 4)); // Only the year
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">Genre: {0}</p>", string.Join(", ", info.genres.Select(x => x.ToString()).ToArray()));
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{0}</p>",
string.IsNullOrEmpty(parentMetaData.Directory.Summary) ? info.summary : parentMetaData.Directory.Summary); // Episode Summary
sb.Append("<td");
sb.Append("<hr>");
sb.Append("<br>");
sb.Append("<br>");
sb.Append("</tr>");
}
sb.Append("</table><br/><br/>");
}
private void Send(string html, PlexSettings plexSettings, bool testEmail = false)
private void Send(NewletterSettings newletterSettings, string html, PlexSettings plexSettings, bool testEmail = false)
{
var settings = EmailSettings.GetSettings();
@ -230,13 +254,24 @@ namespace PlexRequests.Services.Jobs
if (!testEmail)
{
var users = Api.GetUsers(plexSettings.PlexAuthToken);
foreach (var user in users.User)
if (newletterSettings.SendToPlexUsers)
{
message.Bcc.Add(new MailboxAddress(user.Username, user.Email));
var users = Api.GetUsers(plexSettings.PlexAuthToken);
foreach (var user in users.User)
{
message.Bcc.Add(new MailboxAddress(user.Username, user.Email));
}
}
if (newletterSettings.CustomUsersEmailAddresses.Any())
{
foreach (var user in newletterSettings.CustomUsersEmailAddresses)
{
message.Bcc.Add(new MailboxAddress(user, user));
}
}
}
message.Bcc.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender)); // Include the admin
message.Bcc.Add(new MailboxAddress(settings.EmailUsername, settings.RecipientEmail)); // Include the admin
message.From.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender));
try
@ -263,5 +298,15 @@ namespace PlexRequests.Services.Jobs
Log.Error(e);
}
}
private void EndLoopHtml(StringBuilder sb)
{
sb.Append("<td");
sb.Append("<hr>");
sb.Append("<br>");
sb.Append("<br>");
sb.Append("</tr>");
}
}
}

@ -144,7 +144,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr>
<td align="center">
<img src="http://i.imgur.com/s4nswSA.png?" width="400px" text-align="center" />
<img src="http://i.imgur.com/ROTp8mn.png" text-align="center" />
</td>
</tr>
<tr>

@ -80,6 +80,7 @@
<ItemGroup>
<Compile Include="Interfaces\IJobRecord.cs" />
<Compile Include="Interfaces\INotificationEngine.cs" />
<Compile Include="Jobs\HtmlTemplateGenerator.cs" />
<Compile Include="Jobs\IRecentlyAdded.cs" />
<Compile Include="Jobs\JobRecord.cs" />
<Compile Include="Jobs\JobNames.cs" />

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using PlexRequests.Store.Models.Plex;
namespace PlexRequests.Store
{
public interface IPlexDatabase
{
IEnumerable<MetadataItems> GetMetadata();
string DbLocation { get; set; }
Task<IEnumerable<MetadataItems>> GetMetadataAsync();
IEnumerable<MetadataItems> QueryMetadataItems(string query, object param);
}
}

@ -0,0 +1,68 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: MetadataItems.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 Dapper;
using Dapper.Contrib.Extensions;
namespace PlexRequests.Store.Models.Plex
{
[Table("metadata_items")]
public class MetadataItems
{
[Key]
public int id { get; set; }
public int library_section_id { get; set; }
public int parent_id { get; set; }
public int metadata_type { get; set; }
public string guid { get; set; }
public int media_item_count { get; set; }
public string title { get; set; }
public string title_sort { get; set; }
public string original_title { get; set; }
public string studio { get; set; }
public float rating { get; set; }
public int rating_count { get; set; }
public string tagline { get; set; }
public string summary { get; set; }
public string trivia { get; set; }
public string quotes { get; set; }
public string content_rating { get; set; }
public int content_rating_age { get; set; }
public int Index { get; set; }
public string tags_genre { get; set; }
// SKIP Until Date Times
public DateTime originally_available_at { get; set; }
public DateTime available_at { get; set; }
public DateTime expires_at { get; set; }
// Skip RefreshedAt and Year
public DateTime added_at { get; set; }
public string SeriesTitle { get; set; } // Only used in a custom query for the TV Shows
}
}

@ -0,0 +1,94 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: PlexDatabase.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.Data;
using System.IO;
using System.Threading.Tasks;
using Dapper;
using Dapper.Contrib.Extensions;
using Mono.Data.Sqlite;
using PlexRequests.Store.Models.Plex;
namespace PlexRequests.Store
{
/// <summary>
/// We should only ever READ, NEVER WRITE!
/// </summary>
public class PlexDatabase : IPlexDatabase
{
public PlexDatabase(SqliteFactory provider)
{
Factory = provider;
}
private SqliteFactory Factory { get; }
/// <summary>
/// https://support.plex.tv/hc/en-us/articles/202915258-Where-is-the-Plex-Media-Server-data-directory-located-
/// </summary>
public string DbLocation { get; set; }
private IDbConnection DbConnection()
{
var fact = Factory.CreateConnection();
if (fact == null)
{
throw new SqliteException("Factory returned null");
}
fact.ConnectionString = "Data Source=" + DbLocation;
return fact;
}
public IEnumerable<MetadataItems> GetMetadata()
{
using (var con = DbConnection())
{
return con.GetAll<MetadataItems>();
}
}
public async Task<IEnumerable<MetadataItems>> GetMetadataAsync()
{
using (var con = DbConnection())
{
return await con.GetAllAsync<MetadataItems>();
}
}
public IEnumerable<MetadataItems> QueryMetadataItems(string query, object param)
{
using (var con = DbConnection())
{
con.Open();
var data = con.Query<MetadataItems>(query, param);
con.Close();
return data;
}
}
}
}

@ -64,12 +64,15 @@
<ItemGroup>
<Compile Include="DbConfiguration.cs" />
<Compile Include="Entity.cs" />
<Compile Include="IPlexDatabase.cs" />
<Compile Include="Models\IssueBlobs.cs" />
<Compile Include="Models\PlexEpisodes.cs" />
<Compile Include="Models\PlexUsers.cs" />
<Compile Include="Models\Plex\MetadataItems.cs" />
<Compile Include="Models\ScheduledJobs.cs" />
<Compile Include="Models\RequestLimit.cs" />
<Compile Include="Models\UsersToNotify.cs" />
<Compile Include="PlexDatabase.cs" />
<Compile Include="Repository\BaseGenericRepository.cs" />
<Compile Include="Repository\IRequestRepository.cs" />
<Compile Include="Repository\ISettingsRepository.cs" />
@ -84,6 +87,7 @@
<Compile Include="Repository\GenericRepository.cs" />
<Compile Include="RequestedModel.cs" />
<Compile Include="UserEntity.cs" />
<Compile Include="UserLogins.cs" />
<Compile Include="UsersModel.cs" />
<Compile Include="UserRepository.cs" />
<Compile Include="Sql.Designer.cs">
@ -120,6 +124,7 @@
<Name>PlexRequests.Helpers</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

@ -27,13 +27,11 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dapper;
using Dapper.Contrib.Extensions;
using Dapper;
using Mono.Data.Sqlite;

@ -11,6 +11,15 @@ CREATE TABLE IF NOT EXISTS Users
UserProperties BLOB
);
CREATE TABLE IF NOT EXISTS UserLogins
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
UserId varchar(50) NOT NULL ,
Type INTEGER NOT NULL,
LastLoggedIn varchar(100) NOT NULL
);
CREATE INDEX IF NOT EXISTS UserLogins_UserId ON UserLogins (UserId);
CREATE TABLE IF NOT EXISTS GlobalSettings
(
@ -59,6 +68,12 @@ CREATE TABLE IF NOT EXISTS DBInfo
SchemaVersion INTEGER
);
CREATE TABLE IF NOT EXISTS VersionInfo
(
Version INTEGER NOT NULL,
Description VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS ScheduledJobs
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,

@ -24,6 +24,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ***********************************************************************
#endregion
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Dapper;
@ -37,14 +39,14 @@ namespace PlexRequests.Store
/// Creates the tables located in the SqlTables.sql file.
/// </summary>
/// <param name="connection">The connection.</param>
public static void CreateTables(IDbConnection connection)
public static void CreateTables(this IDbConnection connection)
{
connection.Open();
connection.Execute(Sql.SqlTables);
connection.Close();
}
public static void DropTable(IDbConnection con, string tableName)
public static void DropTable(this IDbConnection con, string tableName)
{
using (con)
{
@ -55,7 +57,7 @@ namespace PlexRequests.Store
}
}
public static void AddColumn(IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType)
public static void AddColumn(this IDbConnection connection, string tableName, string alterType, string newColumn, bool allowNulls, string dataType)
{
connection.Open();
var result = connection.Query<TableInfo>($"PRAGMA table_info({tableName});");
@ -65,7 +67,7 @@ namespace PlexRequests.Store
}
var query = $"ALTER TABLE {tableName} {alterType} {newColumn} {dataType}";
if (isNullable)
if (!allowNulls)
{
query = query + " NOT NULL";
}
@ -75,7 +77,7 @@ namespace PlexRequests.Store
connection.Close();
}
public static void Vacuum(IDbConnection con)
public static void Vacuum(this IDbConnection con)
{
using (con)
{
@ -109,7 +111,29 @@ namespace PlexRequests.Store
con.Close();
}
public static IEnumerable<VersionInfo> GetVersionInfo(this IDbConnection con)
{
con.Open();
var result = con.Query<VersionInfo>("SELECT * FROM VersionInfo");
con.Close();
return result;
}
public static void AddVersionInfo(this IDbConnection con, VersionInfo ver)
{
con.Open();
con.Insert(ver);
con.Close();
}
[Table("VersionInfo")]
public class VersionInfo
{
public int Version { get; set; }
public string Description { get; set; }
}
[Table("DBInfo")]
public class DbInfo

@ -0,0 +1,41 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UserLogins.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 Dapper.Contrib.Extensions;
using PlexRequests.Helpers;
namespace PlexRequests.Store
{
[Table("UserLogins")]
public class UserLogins : Entity
{
public string UserId { get; set; }
public UserType Type { get; set; }
public DateTime LastLoggedIn { get; set; }
}
}

@ -1,39 +1,41 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UserModel.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
{
[Table("Users")]
public class UsersModel : UserEntity
{
public byte[] Hash { get; set; }
public byte[] Salt { get; set; }
public byte[] Claims { get; set; }
public byte[] UserProperties { get; set; }
}
}
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: UserModel.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 Dapper.Contrib.Extensions;
namespace PlexRequests.Store
{
[Table("Users")]
public class UsersModel : UserEntity
{
public byte[] Hash { get; set; }
public byte[] Salt { get; set; }
public byte[] Claims { get; set; }
public byte[] UserProperties { get; set; }
}
}

@ -63,6 +63,7 @@ namespace PlexRequests.UI.Tests
}
[Test]
[Ignore("Needs work")]
public async Task HappyPathSendSeriesToSonarrAllSeason()
{
var seriesResult = new SonarrAddSeries() { title = "ABC"};

@ -179,3 +179,6 @@ button.list-group-item:focus {
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-bottom: 6px solid #333333 !important; }
#sidebar-wrapper {
background: #252424; }

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

@ -2,7 +2,9 @@
$primary-colour-outline: #ff761b;
$bg-colour: #333333;
$bg-colour-disabled: #252424;
$i: !important;
$i:
!important
;
.form-control-custom {
background-color: $bg-colour $i;
@ -201,18 +203,18 @@ button.list-group-item:focus {
}
.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;
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 {
@ -221,4 +223,8 @@ button.list-group-item:focus {
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-bottom: 6px solid $bg-colour $i;
}
}
#sidebar-wrapper {
background: $bg-colour-disabled;
}

@ -1,3 +1,4 @@
(function() {
module = angular.module('PlexRequests', []);
module.constant("moment", moment);
}());

@ -1,6 +1,6 @@
(function () {
var controller = function ($scope, userManagementService) {
var controller = function ($scope, userManagementService, moment) {
$scope.user = {}; // The local user
$scope.users = []; // list of users
@ -9,6 +9,7 @@
$scope.selectedUser = {}; // User on the right side
$scope.selectedClaims = {};
$scope.minDate = "0001-01-01T00:00:00.0000000+00:00";
$scope.sortType = "username";
$scope.sortReverse = false;
@ -20,9 +21,19 @@
errorMessage: ""
};
var open = false;
// Select a user to populate on the right side
$scope.selectUser = function (id) {
$scope.selectedUser = $scope.users.find(x => x.id === id);
var user = $scope.users.filter(function (item) {
return item.id === id;
});
$scope.selectedUser = user[0];
if (!open) {
$("#wrapper").toggleClass("toggled");
open = true;
}
}
// Get all users in the system
@ -51,19 +62,38 @@
return;
}
if (!$scope.selectedClaims) {
$scope.error.error = true;
$scope.error.errorMessage = "Please select a permission";
generateNotify($scope.error.errorMessage, 'warning');
return;
}
userManagementService.addUser($scope.user, $scope.selectedClaims)
.then(function (data) {
if (data.message) {
$scope.error.error = true;
$scope.error.errorMessage = data.message;
} else {
$scope.users.push(data); // Push the new user into the array to update the DOM
$scope.users.push(data.data); // Push the new user into the array to update the DOM
$scope.user = {};
$scope.selectedClaims = {};
$scope.claims.forEach(function (entry) {
entry.selected = false;
});
}
});
};
$scope.hasClaim = function (claim) {
var claims = $scope.selectedUser.claimsArray;
var result = claims.some(function (item) {
return item === claim.name;
});
return result;
};
$scope.$watch('claims|filter:{selected:true}',
function (nv) {
$scope.selectedClaims = nv.map(function (claim) {
@ -74,13 +104,36 @@
$scope.updateUser = function () {
var u = $scope.selectedUser;
userManagementService.updateUser(u.id, u.claimsItem, u.alias, u.emailAddress)
.then(function (data) {
if (data) {
$scope.selectedUser = data;
return successCallback("Updated User", "success");
}
});
}
$scope.deleteUser = function () {
var u = $scope.selectedUser;
var result = userManagementService.deleteUser(u.id);
result.success(function(data) {
if (data.result) {
removeUser(u.id, true);
return successCallback("Deleted User", "success");
}
});
}
function getBaseUrl() {
return $('#baseUrl').val();
}
$scope.formatDate = function (utcDate) {
return moment.utc(utcDate).local().format('lll');
}
// On page load
$scope.init = function () {
@ -88,7 +141,21 @@
$scope.getClaims();
return;
}
function removeUser(id, current) {
$scope.users = $scope.users.filter(function (user) {
return user.id !== id;
});
if (current) {
$scope.selectedUser = null;
}
}
}
angular.module('PlexRequests').controller('userManagementController', ["$scope", "userManagementService", controller]);
function successCallback(message, type) {
generateNotify(message, type);
};
angular.module('PlexRequests').controller('userManagementController', ["$scope", "userManagementService","moment", controller]);
}());

@ -24,10 +24,28 @@
return $http.get('/usermanagement/claims');
}
var updateUser = function (id, claims, alias, email) {
return $http({
url: '/usermanagement/updateUser',
method: "POST",
data: { id: id, claims: claims, alias: alias, emailAddress: email }
});
}
var deleteUser = function (id) {
return $http({
url: '/usermanagement/deleteUser',
method: "POST",
data: { id: id }
});
}
return {
getUsers: getUsers,
addUser: addUser,
getClaims: getClaims
getClaims: getClaims,
updateUser: updateUser,
deleteUser: deleteUser
};
}

@ -344,11 +344,95 @@ label {
.img-circle {
border-radius: 50%; }
.user-management-menu {
border-style: ridge;
#wrapper {
padding-left: 0;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease; }
#wrapper.toggled {
padding-right: 250px; }
#sidebar-wrapper {
z-index: 1000;
position: fixed;
right: 250px;
width: 0;
height: 100%;
left: 60%;
margin-right: -250px;
overflow-y: auto;
background: #4e5d6c;
padding-left: 15px;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease; }
#wrapper.toggled #sidebar-wrapper {
width: 500px; }
#page-content-wrapper {
width: 100%;
position: absolute;
padding: 15px; }
#wrapper.toggled #page-content-wrapper {
position: absolute;
margin-left: -250px; }
/* Sidebar Styles */
.sidebar-nav {
position: absolute;
width: 40%;
top: 7%; }
top: 0;
width: 500px;
margin: 0;
padding-left: 0;
list-style: none; }
.sidebar-nav li {
text-indent: 20px;
line-height: 40px; }
.sidebar-nav li a {
display: block;
text-decoration: none;
color: #999999; }
.sidebar-nav li a:hover {
text-decoration: none;
color: #fff;
background: rgba(255, 255, 255, 0.2); }
.sidebar-nav li a:active,
.sidebar-nav li a:focus {
text-decoration: none; }
.sidebar-nav > .sidebar-brand {
height: 65px;
font-size: 18px;
line-height: 60px; }
.sidebar-nav > .sidebar-brand a {
color: #999999; }
.sidebar-nav > .sidebar-brand a:hover {
color: #fff;
background: none; }
@media (min-width: 768px) {
#wrapper {
padding-right: 250px; }
#wrapper.toggled {
padding-right: 0; }
#sidebar-wrapper {
width: 500px; }
#wrapper.toggled #sidebar-wrapper {
width: 0; }
#page-content-wrapper {
padding: 20px;
position: relative; }
#wrapper.toggled #page-content-wrapper {
position: relative;
margin-right: 0; } }

File diff suppressed because one or more lines are too long

@ -434,11 +434,121 @@ $border-radius: 10px;
border-radius: 50%;
}
.user-management-menu {
border-style: ridge;
#wrapper {
padding-left: 0;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease;
}
#wrapper.toggled {
padding-right: 250px;
}
#sidebar-wrapper {
z-index: 1000;
position: fixed;
right: 250px;
width: 0;
height: 100%;
left: 60%;
margin-right: -250px;
overflow-y: auto;
background: #4e5d6c;
padding-left:15px;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease;
}
#wrapper.toggled #sidebar-wrapper {
width: 500px;
}
#page-content-wrapper {
width: 100%;
position: absolute;
padding: 15px;
}
#wrapper.toggled #page-content-wrapper {
position: absolute;
width: 40%;
top: 7%;
margin-left: -250px;
}
/* Sidebar Styles */
.sidebar-nav {
position: absolute;
top: 0;
width: 500px;
margin: 0;
padding-left: 0;
list-style: none;
}
.sidebar-nav li {
text-indent: 20px;
line-height: 40px;
}
.sidebar-nav li a {
display: block;
text-decoration: none;
color: #999999;
}
.sidebar-nav li a:hover {
text-decoration: none;
color: #fff;
background: rgba(255,255,255,0.2);
}
.sidebar-nav li a:active,
.sidebar-nav li a:focus {
text-decoration: none;
}
.sidebar-nav > .sidebar-brand {
height: 65px;
font-size: 18px;
line-height: 60px;
}
.sidebar-nav > .sidebar-brand a {
color: #999999;
}
.sidebar-nav > .sidebar-brand a:hover {
color: #fff;
background: none;
}
@media(min-width:768px) {
#wrapper {
padding-right: 250px;
}
#wrapper.toggled {
padding-right: 0;
}
#sidebar-wrapper {
width: 500px;
}
#wrapper.toggled #sidebar-wrapper {
width: 0;
}
#page-content-wrapper {
padding: 20px;
position: relative;
}
#wrapper.toggled #page-content-wrapper {
position: relative;
margin-right: 0;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -5,6 +5,7 @@
return !opts ? null : opts.inverse(this);
});
var searchSource = $("#search-template").html();
var albumSource = $("#album-template").html();
var searchTemplate = Handlebars.compile(searchSource);

@ -133,14 +133,6 @@ namespace PlexRequests.UI.Helpers
var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation);
var settings = GetSettings();
if (string.IsNullOrEmpty(settings.ThemeName))
{
settings.ThemeName = Themes.PlexTheme;
}
if (settings.ThemeName == "PlexBootstrap.css") settings.ThemeName = Themes.PlexTheme;
if (settings.ThemeName == "OriginalBootstrap.css") settings.ThemeName = Themes.OriginalTheme;
var startUrl = $"{content}/Content";
sb.AppendLine($"<script src=\"{startUrl}/angular.min.js\"></script>"); // Load angular first
@ -161,6 +153,20 @@ namespace PlexRequests.UI.Helpers
return helper.Raw(sb.ToString());
}
public static IHtmlString LoadSettingsAssets(this HtmlHelpers helper)
{
var sb = new StringBuilder();
var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation);
sb.AppendLine($"<script src=\"{content}/Content/bootstrap-switch.min.js\" type=\"text/javascript\"></script>");
sb.AppendLine($"<link rel=\"stylesheet\" href=\"{content}/Content/bootstrap-switch.min.css\" type=\"text/css\"/>");
return helper.Raw(sb.ToString());
}
public static IHtmlString LoadRequestAssets(this HtmlHelpers helper)
{
var sb = new StringBuilder();
@ -212,12 +218,14 @@ namespace PlexRequests.UI.Helpers
{
var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation);
var sb = new StringBuilder();
var controller = $"<script src=\"{content}/Content/app/userManagement/userManagementController.js?v={Assembly}\" type=\"text/javascript\"></script>";
controller += $"<script src=\"{content}/Content/app/userManagement/userManagementService.js?v={Assembly}\" type=\"text/javascript\"></script>";
sb.Append($"<script src=\"{content}/Content/app/userManagement/userManagementController.js?v={Assembly}\" type=\"text/javascript\"></script>");
sb.Append($"<script src=\"{content}/Content/app/userManagement/userManagementService.js?v={Assembly}\" type=\"text/javascript\"></script>");
sb.Append($"<script src=\"{content}/Content/moment.min.js\"></script>");
return helper.Raw(controller);
return helper.Raw(sb.ToString());
}

@ -0,0 +1,49 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: HtmlSecurityHelper.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.Security;
using Nancy.ViewEngines.Razor;
namespace PlexRequests.UI.Helpers
{
public static class HtmlSecurityHelper
{
public static bool HasAnyPermission(this HtmlHelpers helper, params string[] claims)
{
if (!helper.CurrentUser.IsAuthenticated())
{
return false;
}
return helper.CurrentUser.HasAnyClaim(claims);
}
public static bool DoesNotHaveAnyPermission(this HtmlHelpers helper, params string[] claims)
{
return SecurityExtensions.DoesNotHaveClaims(claims, helper.CurrentUser);
}
}
}

@ -0,0 +1,170 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SecurityExtensions.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using Nancy;
using Nancy.Extensions;
using Nancy.Security;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Helpers
{
public static class SecurityExtensions
{
public static bool IsLoggedIn(this NancyContext context)
{
var userName = context.Request.Session[SessionKeys.UsernameKey];
var realUser = false;
var plexUser = userName != null;
if (context.CurrentUser?.IsAuthenticated() ?? false)
{
realUser = true;
}
return realUser || plexUser;
}
public static bool IsPlexUser(this NancyContext context)
{
var userName = context.Request.Session[SessionKeys.UsernameKey];
var plexUser = userName != null;
var isAuth = context.CurrentUser?.IsAuthenticated() ?? false;
return plexUser && !isAuth;
}
public static bool IsNormalUser(this NancyContext context)
{
var userName = context.Request.Session[SessionKeys.UsernameKey];
var plexUser = userName != null;
var isAuth = context.CurrentUser?.IsAuthenticated() ?? false;
return isAuth && !plexUser;
}
/// <summary>
/// This module requires authentication and NO certain claims to be present.
/// </summary>
/// <param name="module">Module to enable</param>
/// <param name="requiredClaims">Claim(s) required</param>
public static void DoesNotHaveClaim(this INancyModule module, params string[] bannedClaims)
{
module.AddBeforeHookOrExecute(SecurityHooks.RequiresAuthentication(), "Requires Authentication");
module.AddBeforeHookOrExecute(DoesNotHaveClaims(bannedClaims), "Has Banned Claims");
}
public static bool DoesNotHaveClaimCheck(this INancyModule module, params string[] bannedClaims)
{
if (!module.Context?.CurrentUser?.IsAuthenticated() ?? false)
{
return false;
}
if (DoesNotHaveClaims(bannedClaims, module.Context))
{
return false;
}
return true;
}
public static bool DoesNotHaveClaimCheck(this NancyContext context, params string[] bannedClaims)
{
if (!context?.CurrentUser?.IsAuthenticated() ?? false)
{
return false;
}
if (DoesNotHaveClaims(bannedClaims, context))
{
return false;
}
return true;
}
/// <summary>
/// Creates a hook to be used in a pipeline before a route handler to ensure
/// that the request was made by an authenticated user does not have the claims.
/// </summary>
/// <param name="claims">Claims the authenticated user needs to have</param>
/// <returns>Hook that returns an Unauthorized response if the user is not
/// authenticated or does have the claims, null otherwise</returns>
private static Func<NancyContext, Response> DoesNotHaveClaims(IEnumerable<string> claims)
{
return ForbiddenIfNot(ctx => !ctx.CurrentUser.HasAnyClaim(claims));
}
public static bool DoesNotHaveClaims(IEnumerable<string> claims, NancyContext ctx)
{
return !ctx.CurrentUser.HasAnyClaim(claims);
}
public static bool DoesNotHaveClaims(IEnumerable<string> claims, IUserIdentity identity)
{
return !identity?.HasAnyClaim(claims) ?? true;
}
// BELOW IS A COPY FROM THE SecurityHooks CLASS!
/// <summary>
/// Creates a hook to be used in a pipeline before a route handler to ensure that
/// the request satisfies a specific test.
/// </summary>
/// <param name="test">Test that must return true for the request to continue</param>
/// <returns>Hook that returns an Forbidden response if the test fails, null otherwise</returns>
private static Func<NancyContext, Response> ForbiddenIfNot(Func<NancyContext, bool> test)
{
return HttpStatusCodeIfNot(HttpStatusCode.Forbidden, test);
}
/// <summary>
/// Creates a hook to be used in a pipeline before a route handler to ensure that
/// the request satisfies a specific test.
/// </summary>
/// <param name="statusCode">HttpStatusCode to use for the response</param>
/// <param name="test">Test that must return true for the request to continue</param>
/// <returns>Hook that returns a response with a specific HttpStatusCode if the test fails, null otherwise</returns>
private static Func<NancyContext, Response> HttpStatusCodeIfNot(HttpStatusCode statusCode, Func<NancyContext, bool> test)
{
return ctx =>
{
Response response = null;
if (!test(ctx))
response = new Response
{
StatusCode = statusCode
};
return response;
};
}
}
}

@ -26,8 +26,6 @@
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.SickRage;
@ -35,11 +33,8 @@ using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using PlexRequests.Helpers.Exceptions;
namespace PlexRequests.UI.Helpers
{
public class TvSender
@ -58,6 +53,13 @@ namespace PlexRequests.UI.Helpers
return await SendToSonarr(sonarrSettings, model, string.Empty);
}
/// <summary>
/// Broken Way
/// </summary>
/// <param name="sonarrSettings"></param>
/// <param name="model"></param>
/// <param name="qualityId"></param>
/// <returns></returns>
public async Task<SonarrAddSeries> SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model, string qualityId)
{
var qualityProfile = 0;
@ -121,15 +123,23 @@ namespace PlexRequests.UI.Helpers
if (series == null)
{
// Set the series as monitored with a season count as 0 so it doesn't search for anything
SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, model.SeasonList, sonarrSettings.ApiKey,
SonarrApi.AddSeriesNew(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, new int[] {1,2,3,4,5,6,7,8,9,10,11,12,13}, sonarrSettings.ApiKey,
sonarrSettings.FullUri);
await Task.Delay(TimeSpan.FromSeconds(1));
series = await GetSonarrSeries(sonarrSettings, model.ProviderId);
foreach (var s in series.seasons)
{
s.monitored = false;
}
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
if (requestAll ?? false)
{
// Monitor all seasons
@ -138,11 +148,21 @@ namespace PlexRequests.UI.Helpers
season.monitored = true;
}
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
SonarrApi.SearchForSeries(series.id, sonarrSettings.ApiKey, sonarrSettings.FullUri); // Search For all episodes!"
//// This is a work around for this issue: https://github.com/Sonarr/Sonarr/issues/1507
//// The above is the previous code.
//SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
// sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, model.SeasonList, sonarrSettings.ApiKey,
// sonarrSettings.FullUri, true, true);
return new SonarrAddSeries { title = series.title }; // We have updated it
}
if (first ?? false)
{
var firstSeries = (series?.seasons?.OrderBy(x => x.seasonNumber)).FirstOrDefault(x => x.seasonNumber > 0) ?? new Season();

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

@ -54,6 +54,9 @@ namespace PlexRequests.UI.Jobs
private IEnumerable<IJobDetail> CreateJobs()
{
var settingsService = Service.Resolve<ISettingsService<ScheduledJobsSettings>>();
var s = settingsService.GetSettings();
var jobs = new List<IJobDetail>();
var jobList = new List<IJobDetail>
@ -66,9 +69,13 @@ namespace PlexRequests.UI.Jobs
JobBuilder.Create<StoreBackup>().WithIdentity("StoreBackup", "Database").Build(),
JobBuilder.Create<StoreCleanup>().WithIdentity("StoreCleanup", "Database").Build(),
JobBuilder.Create<UserRequestLimitResetter>().WithIdentity("UserRequestLimiter", "Request").Build(),
JobBuilder.Create<RecentlyAdded>().WithIdentity("RecentlyAdded", "Email").Build()
};
if (!string.IsNullOrEmpty(s.RecentlyAddedCron))
{
jobList.Add(JobBuilder.Create<RecentlyAdded>().WithIdentity("RecentlyAddedModel", "Email").Build());
}
jobs.AddRange(jobList);
@ -112,66 +119,76 @@ namespace PlexRequests.UI.Jobs
var plexAvailabilityChecker =
TriggerBuilder.Create()
.WithIdentity("PlexAvailabilityChecker", "Plex")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexAvailabilityChecker).RepeatForever())
.Build();
.WithIdentity("PlexAvailabilityChecker", "Plex")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexAvailabilityChecker).RepeatForever())
.Build();
var srCacher =
TriggerBuilder.Create()
.WithIdentity("SickRageCacher", "Cache")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SickRageCacher).RepeatForever())
.Build();
.WithIdentity("SickRageCacher", "Cache")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SickRageCacher).RepeatForever())
.Build();
var sonarrCacher =
TriggerBuilder.Create()
.WithIdentity("SonarrCacher", "Cache")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SonarrCacher).RepeatForever())
.Build();
.WithIdentity("SonarrCacher", "Cache")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.SonarrCacher).RepeatForever())
.Build();
var cpCacher =
TriggerBuilder.Create()
.WithIdentity("CouchPotatoCacher", "Cache")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.CouchPotatoCacher).RepeatForever())
.Build();
.WithIdentity("CouchPotatoCacher", "Cache")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.CouchPotatoCacher).RepeatForever())
.Build();
var storeBackup =
TriggerBuilder.Create()
.WithIdentity("StoreBackup", "Database")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreBackup).RepeatForever())
.Build();
.WithIdentity("StoreBackup", "Database")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreBackup).RepeatForever())
.Build();
var storeCleanup =
TriggerBuilder.Create()
.WithIdentity("StoreCleanup", "Database")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreCleanup).RepeatForever())
.Build();
.WithIdentity("StoreCleanup", "Database")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(s.StoreCleanup).RepeatForever())
.Build();
var userRequestLimiter =
TriggerBuilder.Create()
.WithIdentity("UserRequestLimiter", "Request")
.StartAt(DateTimeOffset.Now.AddMinutes(5)) // Everything has started on application start, lets wait 5 minutes
.WithSimpleSchedule(x => x.WithIntervalInHours(s.UserRequestLimitResetter).RepeatForever())
.Build();
.WithIdentity("UserRequestLimiter", "Request")
.StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute))
// Everything has started on application start, lets wait 5 minutes
.WithSimpleSchedule(x => x.WithIntervalInHours(s.UserRequestLimitResetter).RepeatForever())
.Build();
var plexEpCacher =
TriggerBuilder.Create()
.WithIdentity("PlexEpisodeCacher", "Cache")
.StartAt(DateTimeOffset.Now.AddMinutes(5))
.WithSimpleSchedule(x => x.WithIntervalInHours(s.PlexEpisodeCacher).RepeatForever())
.Build();
.WithIdentity("PlexEpisodeCacher", "Cache")
.StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute))
.WithSimpleSchedule(x => x.WithIntervalInHours(s.PlexEpisodeCacher).RepeatForever())
.Build();
var cronJob = string.IsNullOrEmpty(s.RecentlyAddedCron);
if (!cronJob)
{
var rencentlyAdded =
TriggerBuilder.Create()
.WithIdentity("RecentlyAddedModel", "Email")
.StartNow()
.WithCronSchedule(s.RecentlyAddedCron)
.WithSimpleSchedule(x => x.WithIntervalInHours(2).RepeatForever())
.Build();
triggers.Add(rencentlyAdded);
}
var rencentlyAdded =
TriggerBuilder.Create()
.WithIdentity("RecentlyAdded", "Email")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(2).RepeatForever())
.Build();
triggers.Add(plexAvailabilityChecker);
@ -182,7 +199,6 @@ namespace PlexRequests.UI.Jobs
triggers.Add(storeCleanup);
triggers.Add(userRequestLimiter);
triggers.Add(plexEpCacher);
triggers.Add(rencentlyAdded);
return triggers;
}

@ -1,50 +1,51 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SearchTvShowViewModel.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;
namespace PlexRequests.UI.Models
{
public class SearchMovieViewModel : SearchViewModel
{
public bool Adult { get; set; }
public string BackdropPath { get; set; }
public List<int> GenreIds { get; set; }
public int Id { get; set; }
public string OriginalLanguage { get; set; }
public string OriginalTitle { get; set; }
public string Overview { get; set; }
public double Popularity { get; set; }
public string PosterPath { get; set; }
public DateTime? ReleaseDate { get; set; }
public string Title { get; set; }
public bool Video { get; set; }
public double VoteAverage { get; set; }
public int VoteCount { get; set; }
}
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SearchTvShowViewModel.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;
namespace PlexRequests.UI.Models
{
public class SearchMovieViewModel : SearchViewModel
{
public bool Adult { get; set; }
public string BackdropPath { get; set; }
public List<int> GenreIds { get; set; }
public int Id { get; set; }
public string OriginalLanguage { get; set; }
public string OriginalTitle { get; set; }
public string Overview { get; set; }
public double Popularity { get; set; }
public string PosterPath { get; set; }
public DateTime? ReleaseDate { get; set; }
public string Title { get; set; }
public bool Video { get; set; }
public double VoteAverage { get; set; }
public int VoteCount { get; set; }
public bool AlreadyInCp { get; set; }
}
}

@ -0,0 +1,33 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: DeleteUserViewModel.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.UI.Models
{
public class DeleteUserViewModel
{
public string Id { get; set; }
}
}

@ -1,5 +1,7 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using PlexRequests.Helpers;
namespace PlexRequests.UI.Models
{
@ -17,6 +19,8 @@ namespace PlexRequests.UI.Models
public string EmailAddress { get; set; }
public UserManagementPlexInformation PlexInfo { get; set; }
public string[] ClaimsArray { get; set; }
public List<UserManagementUpdateModel.ClaimsModel> ClaimsItem { get; set; }
public DateTime LastLoggedIn { get; set; }
}
public class UserManagementPlexInformation
@ -39,11 +43,6 @@ namespace PlexRequests.UI.Models
public string NumLibraries { get; set; }
}
public enum UserType
{
PlexUser,
LocalUser
}
public class UserManagementCreateModel
{
@ -57,5 +56,25 @@ namespace PlexRequests.UI.Models
[JsonProperty("email")]
public string EmailAddress { get; set; }
}
public class UserManagementUpdateModel
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("claims")]
public List<ClaimsModel> Claims { get; set; }
public string Alias { get; set; }
public string EmailAddress { get; set; }
public class ClaimsModel
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("selected")]
public bool Selected { get; set; }
}
}
}

@ -63,7 +63,7 @@ using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
using Quartz;
using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.UI.Modules
@ -80,6 +80,7 @@ namespace PlexRequests.UI.Modules
private ISettingsService<PushbulletNotificationSettings> PushbulletService { get; }
private ISettingsService<PushoverNotificationSettings> PushoverService { get; }
private ISettingsService<HeadphonesSettings> HeadphonesService { get; }
private ISettingsService<NewletterSettings> NewsLetterService { get; }
private ISettingsService<LogSettings> LogService { get; }
private IPlexApi PlexApi { get; }
private ISonarrApi SonarrApi { get; }
@ -96,7 +97,7 @@ namespace PlexRequests.UI.Modules
private IJobRecord JobRecorder { get; }
private IAnalytics Analytics { get; }
private IRecentlyAdded RecentlyAdded { get; }
private ISettingsService<NotificationSettingsV2> NotifySettings { get; }
private ISettingsService<NotificationSettingsV2> NotifySettings { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
public AdminModule(ISettingsService<PlexRequestSettings> prService,
@ -112,6 +113,7 @@ namespace PlexRequests.UI.Modules
PushbulletApi pbApi,
ICouchPotatoApi cpApi,
ISettingsService<PushoverNotificationSettings> pushoverSettings,
ISettingsService<NewletterSettings> newsletter,
IPushoverApi pushoverApi,
IRepository<LogEntity> logsRepo,
INotificationService notify,
@ -139,6 +141,7 @@ namespace PlexRequests.UI.Modules
PushoverApi = pushoverApi;
NotificationService = notify;
HeadphonesService = headphones;
NewsLetterService = newsletter;
LogService = logs;
Cache = cache;
SlackSettings = slackSettings;
@ -200,11 +203,14 @@ namespace PlexRequests.UI.Modules
Get["/headphones"] = _ => Headphones();
Post["/headphones"] = _ => SaveHeadphones();
Get["/newsletter"] = _ => Newsletter();
Post["/newsletter"] = _ => SaveNewsletter();
Post["/createapikey"] = x => CreateApiKey();
Post["/autoupdate"] = x => AutoUpdate();
Post["/testslacknotification", true] = async (x,ct) => await TestSlackNotification();
Post["/testslacknotification", true] = async (x, ct) => await TestSlackNotification();
Get["/slacknotification"] = _ => SlackNotifications();
Post["/slacknotification"] = _ => SaveSlackNotifications();
@ -814,6 +820,33 @@ namespace PlexRequests.UI.Modules
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private Negotiator Newsletter()
{
var settings = NewsLetterService.GetSettings();
return View["NewsletterSettings", settings];
}
private Response SaveNewsletter()
{
var settings = this.Bind<NewletterSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
var error = valid.SendJsonError();
Log.Info("Error validating Headphones settings, message: {0}", error.Message);
return Response.AsJson(error);
}
settings.SendRecentlyAddedEmail = settings.SendRecentlyAddedEmail;
var result = NewsLetterService.SaveSettings(settings);
Log.Info("Saved headphones settings, result: {0}", result);
return Response.AsJson(result
? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Newsletter!" }
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
private Response CreateApiKey()
{
this.RequiresClaims(UserClaims.Admin);
@ -942,7 +975,8 @@ namespace PlexRequests.UI.Modules
SonarrCacher = s.SonarrCacher,
StoreBackup = s.StoreBackup,
StoreCleanup = s.StoreCleanup,
JobRecorder = jobsDict
JobRecorder = jobsDict,
RecentlyAddedCron = s.RecentlyAddedCron
};
return View["SchedulerSettings", model];
}
@ -953,6 +987,21 @@ namespace PlexRequests.UI.Modules
Analytics.TrackEventAsync(Category.Admin, Action.Update, "Update ScheduledJobs", Username, CookieHelper.GetAnalyticClientId(Cookies));
var settings = this.Bind<ScheduledJobsSettings>();
if (!string.IsNullOrEmpty(settings.RecentlyAddedCron))
{
// Validate CRON
var isValid = CronExpression.IsValidExpression(settings.RecentlyAddedCron);
if (!isValid)
{
return Response.AsJson(new JsonResponseModel
{
Result = false,
Message =
$"CRON {settings.RecentlyAddedCron} is not valid. Please ensure you are using a valid CRON."
});
}
}
var result = await ScheduledJobSettings.SaveSettingsAsync(settings);
return Response.AsJson(result

@ -25,7 +25,8 @@
// ************************************************************************/
#endregion
using System;
using System.IO;
using Mono.Data.Sqlite;
using Nancy;
using Nancy.ModelBinding;
using Nancy.Security;
@ -35,6 +36,8 @@ using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
@ -59,7 +62,7 @@ namespace PlexRequests.UI.Modules
Post["/plex"] = _ => PlexTest();
Post["/sickrage"] = _ => SickRageTest();
Post["/headphones"] = _ => HeadphonesTest();
Post["/plexdb"] = _ => TestPlexDb();
}
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@ -223,5 +226,65 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); ;
}
}
private Response TestPlexDb()
{
var settings = this.Bind<PlexSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
try
{
var location = string.Empty;
if (string.IsNullOrEmpty(settings.PlexDatabaseLocationOverride))
{
if (Type.GetType("Mono.Runtime") != null)
{
// Mono
location = Path.Combine("/var/lib/plexmediaserver/Library/Application Support/",
"Plex Media Server", "Plug-in Support", "Databases", "com.plexapp.plugins.library.db");
}
else
{
// Default Windows
location = Path.Combine(Environment.ExpandEnvironmentVariables("%LOCALAPPDATA%"),
"Plex Media Server", "Plug-in Support", "Databases", "com.plexapp.plugins.library.db");
}
}
else
{
location = Path.Combine(settings.PlexDatabaseLocationOverride, "Plug-in Support", "Databases", "com.plexapp.plugins.library.db");
}
if (File.Exists(location))
{
return Response.AsJson(new JsonResponseModel
{
Result = true,
Message = "Found the database!"
});
}
return Response.AsJson(new JsonResponseModel
{
Result = false,
Message = $"Could not find the database at the following full location : {location}"
});
}
catch (Exception e)
{
Log.Warn("Exception thrown when attempting to find the plex database: ");
Log.Warn(e);
var message = $"Could not find Plex's DB, please check your settings. <strong>Exception Message:</strong> {e.Message}";
if (e.InnerException != null)
{
message = $"Could not find Plex's DB, please check your settings. <strong>Exception Message:</strong> {e.InnerException.Message}";
}
return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); ;
}
}
}
}

@ -52,7 +52,7 @@ namespace PlexRequests.UI.Modules
ISettingsService<SonarrSettings> sonarrSettings, ISickRageApi srApi, ISettingsService<SickRageSettings> srSettings,
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr) : base("approval", pr)
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
Service = service;
CpService = cpService;
@ -119,7 +119,7 @@ namespace PlexRequests.UI.Modules
private async Task<Response> RequestTvAndUpdateStatus(RequestedModel request, string qualityId)
{
var sender = new TvSender(SonarrApi, SickRageApi);
var sender = new TvSenderOld(SonarrApi, SickRageApi); // TODO put back
var sonarrSettings = await SonarrSettings.GetSettingsAsync();
if (sonarrSettings.Enabled)
@ -439,7 +439,7 @@ namespace PlexRequests.UI.Modules
}
if (r.Type == RequestType.TvShow)
{
var sender = new TvSender(SonarrApi, SickRageApi);
var sender = new TvSenderOld(SonarrApi, SickRageApi); // TODO put back
var sr = await SickRageSettings.GetSettingsAsync();
var sonarr = await SonarrSettings.GetSettingsAsync();
if (sr.Enabled)

@ -366,7 +366,7 @@ namespace PlexRequests.UI.Modules
{
try
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
var issue = await IssuesService.GetAsync(issueId);
var request = await RequestService.GetAsync(issue.RequestId);
if (request.Id > 0)
@ -399,7 +399,7 @@ namespace PlexRequests.UI.Modules
{
try
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
var issue = await IssuesService.GetAsync(issueId);
issue.IssueStatus = status;
@ -417,7 +417,7 @@ namespace PlexRequests.UI.Modules
private async Task<Negotiator> ClearIssue(int issueId, IssueState state)
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
var issue = await IssuesService.GetAsync(issueId);
var toRemove = issue.Issues.FirstOrDefault(x => x.Issue == state);
@ -430,7 +430,7 @@ namespace PlexRequests.UI.Modules
private async Task<Response> AddNote(int requestId, string noteArea, IssueState state)
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
var issue = await IssuesService.GetAsync(requestId);
if (issue == null)
{

@ -40,13 +40,15 @@ using Nancy.Security;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules
{
public class LoginModule : BaseModule
{
public LoginModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IResourceLinker linker)
public LoginModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IResourceLinker linker, IRepository<UserLogins> userLoginRepo)
: base(pr)
{
UserMapper = m;
@ -101,6 +103,14 @@ namespace PlexRequests.UI.Modules
{
redirect = !string.IsNullOrEmpty(BaseUrl) ? $"/{BaseUrl}/search" : "/search";
}
userLoginRepo.Insert(new UserLogins
{
LastLoggedIn = DateTime.UtcNow,
Type = UserType.LocalUser,
UserId = userId.ToString()
});
return this.LoginAndRedirect(userId.Value, expiry, redirect);
};

@ -260,7 +260,7 @@ namespace PlexRequests.UI.Modules
private async Task<Response> DeleteRequest(int requestid)
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
Analytics.TrackEventAsync(Category.Requests, Action.Delete, "Delete Request", Username, CookieHelper.GetAnalyticClientId(Cookies));
var currentEntity = await Service.GetAsync(requestid);
@ -308,7 +308,7 @@ namespace PlexRequests.UI.Modules
private async Task<Response> ClearIssue(int requestId)
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)
@ -326,7 +326,7 @@ namespace PlexRequests.UI.Modules
private async Task<Response> ChangeRequestAvailability(int requestId, bool available)
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
Analytics.TrackEventAsync(Category.Requests, Action.Update, available ? "Make request available" : "Make request unavailable", Username, CookieHelper.GetAnalyticClientId(Cookies));
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)

@ -308,7 +308,7 @@ namespace PlexRequests.UI.Modules
private async Task<Response> DeleteRequest(int requestid)
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
Analytics.TrackEventAsync(Category.Requests, Action.Delete, "Delete Request", Username, CookieHelper.GetAnalyticClientId(Cookies));
var currentEntity = await Service.GetAsync(requestid);
@ -356,7 +356,7 @@ namespace PlexRequests.UI.Modules
private async Task<Response> ClearIssue(int requestId)
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)
@ -374,7 +374,7 @@ namespace PlexRequests.UI.Modules
private async Task<Response> ChangeRequestAvailability(int requestId, bool available)
{
this.RequiresClaims(UserClaims.Admin);
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
Analytics.TrackEventAsync(Category.Requests, Action.Update, available ? "Make request available" : "Make request unavailable", Username, CookieHelper.GetAnalyticClientId(Cookies));
var originalRequest = await Service.GetAsync(requestId);
if (originalRequest == null)

@ -189,7 +189,7 @@ namespace PlexRequests.UI.Modules
switch (searchType)
{
case MovieSearchType.Search:
var movies = await MovieApi.SearchMovie(searchTerm);
var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false);
apiMovies = movies.Select(x =>
new MovieResult
{
@ -232,8 +232,18 @@ namespace PlexRequests.UI.Modules
var plexMovies = Checker.GetPlexMovies();
var settings = await PrService.GetSettingsAsync();
var viewMovies = new List<SearchMovieViewModel>();
var counter = 0;
foreach (var movie in apiMovies)
{
var imdbId = string.Empty;
if (counter <= 5) // Let's only do it for the first 5 items
{
var movieInfoTask = await MovieApi.GetMovieInformation(movie.Id).ConfigureAwait(false); // TODO needs to be careful about this, it's adding extra time to search...
// https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2
imdbId = movieInfoTask.ImdbId;
counter++;
}
var viewMovie = new SearchMovieViewModel
{
Adult = movie.Adult,
@ -252,7 +262,7 @@ namespace PlexRequests.UI.Modules
VoteCount = movie.VoteCount
};
var canSee = CanUserSeeThisRequest(viewMovie.Id, settings.UsersCanViewOnlyOwnRequests, dbMovies);
var plexMovie = Checker.GetMovie(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString());
var plexMovie = Checker.GetMovie(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString(), imdbId);
if (plexMovie != null)
{
viewMovie.Available = true;
@ -349,8 +359,7 @@ namespace PlexRequests.UI.Modules
providerId = viewT.Id.ToString();
}
var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4),
providerId);
var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), providerId);
if (plexShow != null)
{
viewT.Available = true;
@ -435,6 +444,15 @@ namespace PlexRequests.UI.Modules
private async Task<Response> RequestMovie(int movieId)
{
if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser))
{
return
Response.AsJson(new JsonResponseModel()
{
Result = false,
Message = "Sorry, you do not have the correct permissions to request a movie!"
});
}
var settings = await PrService.GetSettingsAsync();
if (!await CheckRequestLimit(settings, RequestType.Movie))
{
@ -479,7 +497,7 @@ namespace PlexRequests.UI.Modules
Type = RequestType.Movie,
Overview = movieInfo.Overview,
ImdbId = movieInfo.ImdbId,
PosterPath = "https://image.tmdb.org/t/p/w150/" + movieInfo.PosterPath,
PosterPath = movieInfo.PosterPath,
Title = movieInfo.Title,
ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue,
Status = movieInfo.Status,
@ -535,6 +553,15 @@ namespace PlexRequests.UI.Modules
/// <returns></returns>
private async Task<Response> RequestTvShow(int showId, string seasons)
{
if (this.DoesNotHaveClaimCheck(UserClaims.ReadOnlyUser))
{
return
Response.AsJson(new JsonResponseModel()
{
Result = false,
Message = "Sorry, you do not have the correct permissions to request a TV Show!"
});
}
// Get the JSON from the request
var req = (Dictionary<string, object>.ValueCollection)Request.Form.Values;
EpisodeRequestModel episodeModel = null;
@ -590,7 +617,7 @@ namespace PlexRequests.UI.Modules
RequestedUsers = new List<string> { Username },
Issues = IssueState.None,
ImdbId = showInfo.externals?.imdb ?? string.Empty,
SeasonCount = showInfo.seasonCount,
SeasonCount = showInfo.Season.Count,
TvDbId = showId.ToString()
};
@ -703,7 +730,18 @@ namespace PlexRequests.UI.Modules
}
else
{
if (Checker.IsTvShowAvailable(shows.ToArray(), showInfo.name, showInfo.premiered?.Substring(0, 4), providerId, model.SeasonList))
if (plexSettings.EnableTvEpisodeSearching)
{
foreach (var s in showInfo.Season)
{
var result = Checker.IsEpisodeAvailable(showId.ToString(), s.SeasonNumber, s.EpisodeNumber);
if (result)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" });
}
}
}
else if (Checker.IsTvShowAvailable(shows.ToArray(), showInfo.name, showInfo.premiered?.Substring(0, 4), providerId, model.SeasonList))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" });
}
@ -719,7 +757,7 @@ namespace PlexRequests.UI.Modules
{
model.Approved = true;
var s = await sonarrSettings;
var sender = new TvSender(SonarrApi, SickrageApi);
var sender = new TvSenderOld(SonarrApi, SickrageApi); // TODO put back
if (s.Enabled)
{
var result = await sender.SendToSonarr(s, model);

@ -59,7 +59,9 @@ namespace PlexRequests.UI.Modules
{
return Response.AsJson(new JsonUpdateAvailableModel { UpdateAvailable = false });
}
#if DEBUG
return Response.AsJson(new JsonUpdateAvailableModel {UpdateAvailable = false});
#endif
var checker = new StatusChecker();
var release = await Cache.GetOrSetAsync(CacheKeys.LastestProductVersion, async() => await checker.GetStatus(), 30);

@ -42,6 +42,8 @@ using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Models;
@ -52,7 +54,7 @@ namespace PlexRequests.UI.Modules
public class UserLoginModule : BaseModule
{
public UserLoginModule(ISettingsService<AuthenticationSettings> auth, IPlexApi api, ISettingsService<PlexSettings> plexSettings, ISettingsService<PlexRequestSettings> pr,
ISettingsService<LandingPageSettings> lp, IAnalytics a, IResourceLinker linker) : base("userlogin", pr)
ISettingsService<LandingPageSettings> lp, IAnalytics a, IResourceLinker linker, IRepository<UserLogins> userLogins) : base("userlogin", pr)
{
AuthService = auth;
LandingPageSettings = lp;
@ -60,13 +62,14 @@ namespace PlexRequests.UI.Modules
Api = api;
PlexSettings = plexSettings;
Linker = linker;
UserLogins = userLogins;
Get["UserLoginIndex", "/", true] = async (x, ct) =>
{
if (!string.IsNullOrEmpty(Username) || IsAdmin)
{
var url = Linker.BuildRelativeUri(Context, "SearchIndex").ToString();
return Response.AsRedirect(url);
return Response.AsRedirect(url);
}
var settings = await AuthService.GetSettingsAsync();
return View["Index", settings];
@ -82,11 +85,13 @@ namespace PlexRequests.UI.Modules
private IPlexApi Api { get; }
private IResourceLinker Linker { get; }
private IAnalytics Analytics { get; }
private IRepository<UserLogins> UserLogins { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
private async Task<Response> LoginUser()
{
var userId = string.Empty;
var dateTimeOffset = Request.Form.DateTimeOffset;
var username = Request.Form.username.Value;
Log.Debug("Username \"{0}\" attempting to login", username);
@ -135,6 +140,7 @@ namespace PlexRequests.UI.Modules
authenticated = CheckIfUserIsInPlexFriends(username, plexSettings.PlexAuthToken);
Log.Debug("Friends list result = {0}", authenticated);
}
userId = signedIn.user.uuid;
}
}
else if (settings.UserAuthentication) // Check against the users in Plex
@ -147,6 +153,11 @@ namespace PlexRequests.UI.Modules
authenticated = true;
}
Log.Debug("Friends list result = {0}", authenticated);
if (authenticated)
{
// Get the user that is authenticated to store in the UserLogins
userId = GetUserIdIsInPlexFriends(username, plexSettings.PlexAuthToken);
}
}
else if (!settings.UserAuthentication) // No auth, let them pass!
{
@ -156,13 +167,13 @@ namespace PlexRequests.UI.Modules
if (authenticated)
{
UserLogins.Insert(new UserLogins { UserId = userId, Type = UserType.PlexUser, LastLoggedIn = DateTime.UtcNow });
Log.Debug("We are authenticated! Setting session.");
// Add to the session (Used in the BaseModules)
Session[SessionKeys.UsernameKey] = (string)username;
Session[SessionKeys.ClientDateTimeOffsetKey] = (int)dateTimeOffset;
}
Session[SessionKeys.ClientDateTimeOffsetKey] = (int)dateTimeOffset;
if (!authenticated)
{
var uri = Linker.BuildRelativeUri(Context, "UserLoginIndex");
@ -214,6 +225,14 @@ namespace PlexRequests.UI.Modules
return allUsers != null && allUsers.Any(x => x.Title.Equals(username, StringComparison.CurrentCultureIgnoreCase));
}
private string GetUserIdIsInPlexFriends(string username, string authToken)
{
var users = Api.GetUsers(authToken);
var allUsers = users?.User?.Where(x => !string.IsNullOrEmpty(x.Title));
return allUsers?.Where(x => x.Title.Equals(username, StringComparison.CurrentCultureIgnoreCase)).Select(x => x.Id).FirstOrDefault();
}
private bool IsUserInDeniedList(string username, AuthenticationSettings settings)
{
return settings.DeniedUserList.Any(x => x.Equals(username, StringComparison.CurrentCultureIgnoreCase));

@ -13,13 +13,15 @@ using PlexRequests.Core;
using PlexRequests.Core.Models;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules
{
public class UserManagementModule : BaseModule
{
public UserManagementModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService<PlexSettings> plex) : base("usermanagement", pr)
public UserManagementModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService<PlexSettings> plex, IRepository<UserLogins> userLogins) : base("usermanagement", pr)
{
#if !DEBUG
this.RequiresClaims(UserClaims.Admin);
@ -27,6 +29,7 @@ namespace PlexRequests.UI.Modules
UserMapper = m;
PlexApi = plexApi;
PlexSettings = plex;
UserLoginsRepo = userLogins;
Get["/"] = x => Load();
@ -35,11 +38,14 @@ namespace PlexRequests.UI.Modules
Get["/local/{id}"] = x => LocalDetails((Guid)x.id);
Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id);
Get["/claims"] = x => GetClaims();
Post["/updateuser"] = x => UpdateUser();
Post["/deleteuser"] = x => DeleteUser();
}
private ICustomUserMapper UserMapper { get; }
private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private IRepository<UserLogins> UserLoginsRepo { get; }
private Negotiator Load()
{
@ -50,22 +56,13 @@ namespace PlexRequests.UI.Modules
{
var localUsers = await UserMapper.GetUsersAsync();
var model = new List<UserManagementUsersViewModel>();
foreach (var user in localUsers)
{
var claims = ByteConverterHelper.ReturnObject<string[]>(user.Claims);
var claimsString = string.Join(", ", claims);
var userProps = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties);
var usersDb = UserLoginsRepo.GetAll().ToList();
model.Add(new UserManagementUsersViewModel
{
Id = user.UserGuid,
Claims = claimsString,
Username = user.UserName,
Type = UserType.LocalUser,
EmailAddress = userProps.EmailAddress,
ClaimsArray = claims
});
foreach (var user in localUsers)
{
var userDb = usersDb.FirstOrDefault(x => x.UserId == user.UserGuid);
model.Add(MapLocalUser(user, userDb?.LastLoggedIn ?? DateTime.MinValue));
}
var plexSettings = await PlexSettings.GetSettingsAsync();
@ -76,7 +73,7 @@ namespace PlexRequests.UI.Modules
foreach (var u in plexUsers.User)
{
var userDb = usersDb.FirstOrDefault(x => x.UserId == u.Id);
model.Add(new UserManagementUsersViewModel
{
Username = u.Username,
@ -87,7 +84,8 @@ namespace PlexRequests.UI.Modules
PlexInfo = new UserManagementPlexInformation
{
Thumb = u.Thumb
}
},
LastLoggedIn = userDb?.LastLoggedIn ?? DateTime.MinValue,
});
}
}
@ -115,12 +113,80 @@ namespace PlexRequests.UI.Modules
var user = UserMapper.CreateUser(model.Username, model.Password, model.Claims, new UserProperties { EmailAddress = model.EmailAddress });
if (user.HasValue)
{
return Response.AsJson(user);
return Response.AsJson(MapLocalUser(UserMapper.GetUser(user.Value), DateTime.MinValue));
}
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not save user" });
}
private Response UpdateUser()
{
var body = Request.Body.AsString();
if (string.IsNullOrEmpty(body))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not save user, invalid JSON body" });
}
var model = JsonConvert.DeserializeObject<UserManagementUpdateModel>(body);
if (string.IsNullOrWhiteSpace(model.Id))
{
return Response.AsJson(new JsonResponseModel
{
Result = true,
Message = "Couldn't find the user"
});
}
var claims = new List<string>();
foreach (var c in model.Claims)
{
if (c.Selected)
{
claims.Add(c.Name);
}
}
var userFound = UserMapper.GetUser(new Guid(model.Id));
userFound.Claims = ByteConverterHelper.ReturnBytes(claims.ToArray());
var currentProps = ByteConverterHelper.ReturnObject<UserProperties>(userFound.UserProperties);
currentProps.UserAlias = model.Alias;
currentProps.EmailAddress = model.EmailAddress;
userFound.UserProperties = ByteConverterHelper.ReturnBytes(currentProps);
var user = UserMapper.EditUser(userFound);
var dbUser = UserLoginsRepo.GetAll().FirstOrDefault(x => x.UserId == user.UserGuid);
var retUser = MapLocalUser(user, dbUser?.LastLoggedIn ?? DateTime.MinValue);
return Response.AsJson(retUser);
}
private Response DeleteUser()
{
var body = Request.Body.AsString();
if (string.IsNullOrEmpty(body))
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not save user, invalid JSON body" });
}
var model = JsonConvert.DeserializeObject<DeleteUserViewModel>(body);
if (string.IsNullOrWhiteSpace(model.Id))
{
return Response.AsJson(new JsonResponseModel
{
Result = true,
Message = "Couldn't find the user"
});
}
UserMapper.DeleteUser(model.Id);
return Response.AsJson(new JsonResponseModel {Result = true});
}
private Response LocalDetails(Guid id)
{
var localUser = UserMapper.GetUser(id);
@ -165,6 +231,45 @@ namespace PlexRequests.UI.Modules
}
return Response.AsJson(retVal);
}
private UserManagementUsersViewModel MapLocalUser(UsersModel user, DateTime lastLoggedIn)
{
var claims = ByteConverterHelper.ReturnObject<string[]>(user.Claims);
var claimsString = string.Join(", ", claims);
var userProps = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties);
var m = new UserManagementUsersViewModel
{
Id = user.UserGuid,
Claims = claimsString,
Username = user.UserName,
Type = UserType.LocalUser,
EmailAddress = userProps.EmailAddress,
Alias = userProps.UserAlias,
ClaimsArray = claims,
ClaimsItem = new List<UserManagementUpdateModel.ClaimsModel>(),
LastLoggedIn = lastLoggedIn
};
// Add all of the current claims
foreach (var c in claims)
{
m.ClaimsItem.Add(new UserManagementUpdateModel.ClaimsModel { Name = c, Selected = true });
}
var allClaims = UserMapper.GetAllClaims();
// Get me the current claims that the user does not have
var missingClaims = allClaims.Except(claims);
// Add them into the view
foreach (var missingClaim in missingClaims)
{
m.ClaimsItem.Add(new UserManagementUpdateModel.ClaimsModel { Name = missingClaim, Selected = false });
}
return m;
}
}
}

@ -32,6 +32,7 @@ using Nancy.Authentication.Forms;
using Ninject.Modules;
using PlexRequests.Core;
using PlexRequests.Core.Migration;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification;
@ -45,6 +46,10 @@ namespace PlexRequests.UI.NinjectModules
{
Bind<ICacheProvider>().To<MemoryCacheProvider>().InSingletonScope();
Bind<ISqliteConfiguration>().To<DbConfiguration>().WithConstructorArgument("provider", new SqliteFactory());
Bind<IPlexDatabase>().To<PlexDatabase>().WithConstructorArgument("provider", new SqliteFactory());
Bind<IPlexReadOnlyDatabase>().To<PlexReadOnlyDatabase>();
Bind<IMigrationRunner>().To<MigrationRunner>();
Bind<IUserMapper>().To<UserMapper>();
Bind<ICustomUserMapper>().To<UserMapper>();

@ -117,6 +117,7 @@
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Web" />
<Reference Include="System.Web.Extensions" />
<Reference Include="Microsoft.CSharp" />
@ -196,8 +197,9 @@
<Reference Include="System.Web.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<HintPath>..\packages\Microsoft.AspNet.Razor.2.0.30506.0\lib\net40\System.Web.Razor.dll</HintPath>
</Reference>
<Reference Include="TMDbLib, Version=0.9.0.0, Culture=neutral, PublicKeyToken=null">
<Reference Include="TMDbLib, Version=0.9.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
@ -210,9 +212,12 @@
<Compile Include="Helpers\EmptyViewBase.cs" />
<Compile Include="Helpers\HeadphonesSender.cs" />
<Compile Include="Helpers\AngularViewBase.cs" />
<Compile Include="Helpers\HtmlSecurityHelper.cs" />
<Compile Include="Helpers\SecurityExtensions.cs" />
<Compile Include="Helpers\ServiceLocator.cs" />
<Compile Include="Helpers\Themes.cs" />
<Compile Include="Helpers\TvSender.cs" />
<Compile Include="Helpers\TvSenderOld.cs" />
<Compile Include="Helpers\ValidationHelper.cs" />
<Compile Include="Jobs\CustomJobFactory.cs" />
<Compile Include="Jobs\Scheduler.cs" />
@ -237,7 +242,8 @@
<Compile Include="Models\SearchViewModel.cs" />
<Compile Include="Models\SearchMusicViewModel.cs" />
<Compile Include="Models\SearchMovieViewModel.cs" />
<Compile Include="Models\UserUpdateViewModel.cs" />
<Compile Include="Models\UserManagement\DeleteUserViewModel.cs" />
<Compile Include="Models\UserManagement\UserUpdateViewModel.cs" />
<Compile Include="Modules\ApiDocsModule.cs" />
<Compile Include="Modules\ApiSettingsMetadataModule.cs" />
<Compile Include="Modules\ApiUserMetadataModule.cs" />
@ -304,6 +310,12 @@
<DependentUpon>base.css</DependentUpon>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\bootstrap-switch.min.css">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Content\bootstrap-switch.min.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Content\bootstrap.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -487,7 +499,7 @@
</Content>
<Compile Include="Modules\ApiRequestModule.cs" />
<Compile Include="Models\ApiModel.cs" />
<Compile Include="Models\UserManagementUsersViewModel.cs" />
<Compile Include="Models\UserManagement\UserManagementUsersViewModel.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Content\bootstrap.min.js">
@ -711,6 +723,9 @@
<Content Include="Views\Admin\NotificationSettings.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="Views\Admin\NewsletterSettings.cshtml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="Web.Debug.config">
<DependentUpon>web.config</DependentUpon>
</None>
@ -745,6 +760,10 @@
<Project>{8CB8D235-2674-442D-9C6A-35FCAEEB160D}</Project>
<Name>PlexRequests.Api</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Core.Migration\PlexRequests.Core.Migration.csproj">
<Project>{8406EE57-D533-47C0-9302-C6B5F8C31E55}</Project>
<Name>PlexRequests.Core.Migration</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Core\PlexRequests.Core.csproj">
<Project>{DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}</Project>
<Name>PlexRequests.Core</Name>

@ -446,4 +446,25 @@
<data name="Custom_Donation_Default" xml:space="preserve">
<value>Donate to Library Maintainer</value>
</data>
<data name="Search_Available_on_plex" xml:space="preserve">
<value>Available on Plex</value>
</data>
<data name="Search_Movie_Status" xml:space="preserve">
<value>Movie status</value>
</data>
<data name="Search_Not_Requested_Yet" xml:space="preserve">
<value>Not Requested yet</value>
</data>
<data name="Search_Pending_approval" xml:space="preserve">
<value>Pending approval</value>
</data>
<data name="Search_Processing_Request" xml:space="preserve">
<value>Processing request</value>
</data>
<data name="Search_Request_denied" xml:space="preserve">
<value>Request denied</value>
</data>
<data name="Search_TV_Show_Status" xml:space="preserve">
<value>TV show status</value>
</data>
</root>

@ -744,6 +744,15 @@ namespace PlexRequests.UI.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Available on Plex.
/// </summary>
public static string Search_Available_on_plex {
get {
return ResourceManager.GetString("Search_Available_on_plex", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Coming Soon.
/// </summary>
@ -834,6 +843,15 @@ namespace PlexRequests.UI.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Movie status.
/// </summary>
public static string Search_Movie_Status {
get {
return ResourceManager.GetString("Search_Movie_Status", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Movies.
/// </summary>
@ -852,6 +870,15 @@ namespace PlexRequests.UI.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Not Requested yet.
/// </summary>
public static string Search_Not_Requested_Yet {
get {
return ResourceManager.GetString("Search_Not_Requested_Yet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to We could not remove this notification because you never had it!.
/// </summary>
@ -870,6 +897,24 @@ namespace PlexRequests.UI.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Pending approval.
/// </summary>
public static string Search_Pending_approval {
get {
return ResourceManager.GetString("Search_Pending_approval", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Processing request.
/// </summary>
public static string Search_Processing_Request {
get {
return ResourceManager.GetString("Search_Processing_Request", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Report Issue.
/// </summary>
@ -888,6 +933,15 @@ namespace PlexRequests.UI.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Request denied.
/// </summary>
public static string Search_Request_denied {
get {
return ResourceManager.GetString("Search_Request_denied", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Requested.
/// </summary>
@ -978,6 +1032,15 @@ namespace PlexRequests.UI.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to TV show status.
/// </summary>
public static string Search_TV_Show_Status {
get {
return ResourceManager.GetString("Search_TV_Show_Status", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The request of TV Shows is not correctly set up. Please contact your admin..
/// </summary>

@ -32,7 +32,8 @@ using Ninject.Planning.Bindings.Resolvers;
using NLog;
using Owin;
using PlexRequests.Core.Migration;
using PlexRequests.Services.Jobs;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Jobs;
using PlexRequests.UI.NinjectModules;
@ -47,26 +48,41 @@ namespace PlexRequests.UI
{
try
{
Debug.WriteLine("Starting StartupConfiguration");
Debug.WriteLine("Starting StartupConfiguration");
var resolver = new DependancyResolver();
Debug.WriteLine("Created DI Resolver");
Debug.WriteLine("Created DI Resolver");
var modules = resolver.GetModules();
Debug.WriteLine("Getting all the modules");
Debug.WriteLine("Getting all the modules");
Debug.WriteLine("Modules found finished.");
Debug.WriteLine("Modules found finished.");
var kernel = new StandardKernel(modules);
Debug.WriteLine("Created Kernel and Injected Modules");
Debug.WriteLine("Created Kernel and Injected Modules");
Debug.WriteLine("Added Contravariant Binder");
Debug.WriteLine("Added Contravariant Binder");
kernel.Components.Add<IBindingResolver, ContravariantBindingResolver>();
Debug.WriteLine("Start the bootstrapper with the Kernel.ı");
app.UseNancy(options => options.Bootstrapper = new Bootstrapper(kernel));
Debug.WriteLine("Finished bootstrapper");
Debug.WriteLine("Start the bootstrapper with the Kernel.");
app.UseNancy(options => options.Bootstrapper = new Bootstrapper(kernel));
Debug.WriteLine("Finished bootstrapper");
Debug.WriteLine("Migrating DB Now");
var runner = kernel.Get<IMigrationRunner>();
runner.MigrateToLatest();
Debug.WriteLine("Settings up Scheduler");
var scheduler = new Scheduler();
scheduler.StartScheduler();
//var c = kernel.Get<IRecentlyAdded>();
//c.Test();
}
catch (Exception exception)
{

@ -54,7 +54,7 @@
<p class="form-group">Notice Message</p>
<div class="form-group">
<div>
<textarea rows="4" type="text" class="form-control-custom form-control " id="NoticeMessage" name="NoticeMessage" placeholder="e.g. Plex will be down for maintaince (HTML is allowed)" value="@Model.NoticeMessage"></textarea>
<textarea rows="4" type="text" class="form-control-custom form-control " id="NoticeMessage" name="NoticeMessage" placeholder="e.g. Plex will be down for maintaince (HTML is allowed)" value="@Model.NoticeMessage">@Model.NoticeMessage</textarea>
</div>
</div>
@ -121,6 +121,8 @@
});
$('#save').click(function (e) {
e.preventDefault();
var start = '';
var end = '';
if ($startDate.data("DateTimePicker").date()) {
@ -130,8 +132,6 @@
end = $endDate.data("DateTimePicker").date().toISOString();
}
e.preventDefault();
var $form = $("#mainForm");
var data = $form.serialize();

@ -0,0 +1,130 @@
@using System.Linq
@using PlexRequests.Core.Models
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.NewletterSettings>
@Html.Partial("_Sidebar")
<div class="col-sm-8 col-sm-push-1">
<form class="form-horizontal" method="POST" id="mainForm">
<fieldset>
<legend>Newsletter Settings</legend>
<!-- Email Nofication Section -->
<div class="form-group">
<div class="checkbox">
<small>Note: This will require you to setup your email notifications</small>
<br />
@if (Model.SendRecentlyAddedEmail)
{
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail" checked="checked"><label for="SendRecentlyAddedEmail">Enable the newsletter of recently added content</label>
}
else
{
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail"><label for="SendRecentlyAddedEmail">Enable the newsletter of recently added content</label>
}
</div>
</div>
<div class="form-group">
<div class="checkbox">
@if (Model.SendToPlexUsers)
{
<input type="checkbox" id="SendToPlexUsers" name="SendToPlexUsers" checked="checked"><label for="SendToPlexUsers">Send to all of your Plex 'Friends'</label>
}
else
{
<input type="checkbox" id="SendToPlexUsers" name="SendToPlexUsers"><label for="SendToPlexUsers">Send to all of your Plex 'Friends'</label>
}
</div>
</div>
<div class="form-group">
<label for="StoreCleanup" class="control-label">Email Addresses to Send to (For users that are not in your Plex Friends)</label>
<div>
<input type="text" class="form-control form-control-custom " placeholder="email@address.com;second@address.com" id="StoreCleanup" name="StoreCleanup" value="@Model.CustomUsers">
</div>
</div>
<div class="form-group">
<div>
<button id="recentlyAddedBtn" class="btn btn-primary-outline">Send test email to Admin <div id="spinner"></div></button>
</div>
</div>
<br />
<br />
<div class="form-group">
<div>
<button type="submit" id="save" class="btn btn-primary-outline">Submit</button>
</div>
</div>
<!-- Email Nofication Section -->
</fieldset>
</form>
</div>
<script>
$(function () {
var base = '@Html.GetBaseUrl()';
$('#save').click(function (e) {
e.preventDefault();
var $form = $("#mainForm");
var data = $form.serialize();
$.ajax({
type: $form.prop("method"),
data: data,
url: $form.prop("action"),
dataType: "json",
success: function (response) {
if (response.result === true) {
generateNotify(response.message, "success");
} else {
generateNotify(response.message, "warning");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
$('#recentlyAddedBtn').click(function (e) {
e.preventDefault();
var base = '@Html.GetBaseUrl()';
var url = createBaseUrl(base, '/admin/recentlyAddedTest');
$('#spinner').attr("class", "fa fa-spinner fa-spin");
$.ajax({
type: "post",
url: url,
dataType: "json",
success: function (response) {
if (response) {
generateNotify(response.message, "success");
$('#spinner').attr("class", "fa fa-check");
} else {
generateNotify(response.message, "danger");
$('#spinner').attr("class", "fa fa-times");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
$('#spinner').attr("class", "fa fa-times");
}
});
});
});
</script>

@ -16,7 +16,7 @@
<form class="form-horizontal" method="POST" id="mainForm">
<fieldset>
<legend>Plex Settings</legend>
@*<input id="advancedToggle" type="checkbox"/>*@ @*TODO*@
<div class="form-group">
<label for="Ip" class="control-label">Plex Hostname or IP</label>
<div>
@ -87,6 +87,21 @@
<input type="text" class="form-control form-control-custom " id="SubDir" name="SubDir" value="@Model.SubDir">
</div>
</div>
@*<div class="form-group">
<label for="PlexDatabaseLocationOverride" class="control-label">Plex Database Override</label>
<div>
<input type="text" class="form-control form-control-custom " id="PlexDatabaseLocationOverride" name="PlexDatabaseLocationOverride" placeholder="%LOCALAPPDATA%\Plex Media Server\" value="@Model.PlexDatabaseLocationOverride">
</div>
<small>
This is your Plex data directory location, if we cannot manually find it then you need to specify the location! See <a href="https://support.plex.tv/hc/en-us/articles/202915258-Where-is-the-Plex-Media-Server-data-directory-located-">Here</a>.
</small>
</div>
<div class="form-group">
<div class="">
<button id="dbTest" class="btn btn-primary-outline">Test Database Directory <i class="fa fa-database"></i> <div id="dbSpinner"></div></button>
</div>
</div>*@
<div class="form-group">
<label for="authToken" class="control-label">Plex Authorization Token</label>
@ -94,6 +109,8 @@
<input type="text" class="form-control-custom form-control" id="authToken" name="PlexAuthToken" placeholder="Plex Auth Token" value="@Model.PlexAuthToken">
</div>
</div>
<div class="form-group">
<label for="username" class="control-label">Username and Password</label>
@ -129,6 +146,9 @@
<script>
$(function () {
$("#advancedToggle").bootstrapSwitch();
var base = '@Html.GetBaseUrl()';
$('#testPlex').click(function (e) {
@ -163,6 +183,38 @@
});
});
$('#dbTest').click(function (e) {
e.preventDefault();
var url = createBaseUrl(base, '/test/plexdb');
var $form = $("#mainForm");
$('#dbSpinner').attr("class", "fa fa-spinner fa-spin");
$.ajax({
type: $form.prop("method"),
url: url,
data: $form.serialize(),
dataType: "json",
success: function (response) {
$('#dbSpinner').attr("class", "");
console.log(response);
if (response.result === true) {
generateNotify(response.message, "success");
$('#dbSpinner').attr("class", "fa fa-check");
} else {
generateNotify(response.message, "warning");
$('#dbSpinner').attr("class", "fa fa-times");
}
},
error: function (e) {
$('#spinner').attr("class", "fa fa-times");
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
$('#requestToken').click(function (e) {
e.preventDefault();
var $form = $("#mainForm");

@ -16,7 +16,7 @@
{
<div class="row">
<div class="col-md-4">@record.Key</div>
<div class="col-md-5 col-md-push-3 date">@record.Value.ToString("O")</div>
<div class="col-md-5 col-md-push-3 date">@record.Value.ToString("R")</div>
</div>
<hr style="margin-top: 4px; margin-bottom: 4px"/>
}
@ -78,11 +78,12 @@
<input type="text" class="form-control form-control-custom " id="UserRequestLimitResetter" name="UserRequestLimitResetter" value="@Model.UserRequestLimitResetter">
</div>
</div>
<small>Please note, this uses a Quartz CRON job, you can build a CRON <a href="http://www.cronmaker.com/">Here</a></small>
<div class="form-group">
<label for="RecentlyAdded" class="control-label">Recently Added Email (hours)</label>
<label for="RecentlyAddedCron" class="control-label">Recently Added Email (CRON)</label>
<div>
<input type="text" class="form-control form-control-custom " id="RecentlyAdded" name="RecentlyAdded" value="@Model.RecentlyAdded">
<input type="text" class="form-control form-control-custom " id="RecentlyAddedCron" name="RecentlyAddedCron" value="@Model.RecentlyAddedCron">
</div>
</div>

@ -74,27 +74,6 @@
</select>
</div>
</div>
<br/>
<br/>
<div class="form-group">
<div class="checkbox">
<small>Note: This will require you to setup your email notifications</small>
@if (Model.SendRecentlyAddedEmail)
{
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail" checked="checked"><label for="SendRecentlyAddedEmail">Send out a weekly email of recently added content to all your Plex 'Friends'</label>
}
else
{
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail"><label for="SendRecentlyAddedEmail">Send out a weekly email of recently added content to all your Plex 'Friends'</label>
}
</div>
</div>
<button id="recentlyAddedBtn" class="btn btn-primary-outline">Send test email to Admin</button>
<br/>
<br/>
<div class="form-group">
<div class="checkbox">
@ -382,7 +361,7 @@
});
});
$('#refreshKey').click(function (e) {
$('#refreshKey').click(function(e) {
e.preventDefault();
var base = '@Html.GetBaseUrl()';
var url = createBaseUrl(base, '/admin/createapikey');
@ -391,37 +370,13 @@
type: "post",
url: url,
dataType: "json",
success: function (response) {
success: function(response) {
if (response) {
generateNotify("Success!", "success");
$('#apiKey').val(response);
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
$('#recentlyAddedBtn').click(function (e) {
e.preventDefault();
var base = '@Html.GetBaseUrl()';
var url = createBaseUrl(base, '/admin/recentlyAddedTest');
$.ajax({
type: "post",
url: url,
dataType: "json",
success: function (response) {
if (response) {
generateNotify(response.message, "success");
} else {
generateNotify(response.message, "danger");
}
},
error: function (e) {
error: function(e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}

@ -1,4 +1,5 @@
@using PlexRequests.UI.Helpers
@Html.LoadSettingsAssets()
<div class="col-lg-3 col-md-3 col-sm-4">
<div class="list-group table-of-contents">
@Html.GetSidebarUrl(Context, "/admin", "Plex Request")
@ -9,6 +10,7 @@
@Html.GetSidebarUrl(Context, "/admin/sonarr", "Sonarr")
@Html.GetSidebarUrl(Context, "/admin/sickrage", "SickRage")
@Html.GetSidebarUrl(Context, "/admin/headphones", "Headphones (Beta)")
@Html.GetSidebarUrl(Context, "/admin/newsletter", "Newsletter Settings")
@Html.GetSidebarUrl(Context, "/admin/emailnotification", "Email Notifications")
@Html.GetSidebarUrl(Context, "/admin/pushbulletnotification", "Pushbullet Notifications")
@Html.GetSidebarUrl(Context, "/admin/pushovernotification", "Pushover Notifications")

@ -168,15 +168,38 @@
<a href="http://www.imdb.com/title/{{imdb}}/" target="_blank">
<h4 class="request-title">{{title}} ({{year}})</h4>
</a>
<span class="label label-success">{{status}}</span>
<div>
{{#if_eq type "tv"}}
<span>@UI.Search_TV_Show_Status: </span>
{{else}}
<span>@UI.Search_Movie_Status: </span>
{{/if_eq}}
<span class="label label-success">{{status}}</span>
</div>
<div>
<span>Request status: </span>
{{#if available}}
<span class="label label-success">@UI.Search_Available_on_plex</span>
{{else}}
{{#if approved}}
<span class="label label-info">@UI.Search_Processing_Request</span>
{{else if denied}}
<span class="label label-danger">@UI.Search_Request_denied</span>
{{#if deniedReason}}
<span class="customTooltip" title="{{deniedReason}}"><i class="fa fa-info-circle"></i></span>
{{/if}}
{{else}}
<span class="label label-warning">@UI.Search_Pending_approval</span>
{{/if}}
{{/if}}
</div>
</div>
<br />
{{#if denied}}
<div>
Denied: <i style="color:red;" class="fa fa-check"></i>
{{#if deniedReason}}
<span class="customTooltip" title="{{deniedReason}}"><i class="fa fa-info-circle"></i></span>
{{/if}}
</div>
{{/if}}
@ -185,31 +208,14 @@
{{else}}
<div>@UI.Requests_ReleaseDate: {{releaseDate}}</div>
{{/if_eq}}
{{#unless denied}}
<div>
@UI.Common_Approved:
{{#if_eq approved false}}
<i id="{{requestId}}notapproved" class="fa fa-times"></i>
{{/if_eq}}
{{#if_eq approved true}}
<i class="fa fa-check"></i>
{{/if_eq}}
</div>
{{/unless}}
<div>
@UI.Requests_Available
{{#if_eq available false}}
<i id="availableIcon{{requestId}}" class="fa fa-times"></i>
{{/if_eq}}
{{#if_eq available true}}
<i id="availableIcon{{requestId}}" class="fa fa-check"></i>
{{/if_eq}}
</div>
<br />
{{#if_eq type "tv"}}
{{#if episodes}}
Episodes: <span class="customTooltip" data-tooltip-content="#{{requestId}}toolTipContent"><i class="fa fa-info-circle"></i></span>
{{else}}
<div>@UI.Requests_SeasonsRequested: {{seriesRequested}}</div>
{{/if}}
{{/if_eq}}
{{#if requestedUsers}}
@ -217,11 +223,10 @@
{{/if}}
<div>@UI.Requests_RequestedDate: {{requestedDate}}</div>
<div>
@UI.Issues_Issue:
{{#if_eq issueId 0}}
<i class="fa fa-times"></i>
@*Nothing*@
{{else}}
<a href="@formAction/issues/{{issueId}}"><i class="fa fa-check"></i></a>
@UI.Issues_Issue: <a href="@formAction/issues/{{issueId}}"><i class="fa fa-check"></i></a>
{{/if_eq}}
</div>
</div>

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

Loading…
Cancel
Save