Merge pull request #618 from tidusjar/dev

Release 1.9.5
This commit is contained in:
Jamie 2016-10-27 23:35:35 +01:00 committed by GitHub
commit ea2f4d3e78
108 changed files with 4039 additions and 854 deletions

View file

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

View file

@ -39,6 +39,10 @@ namespace PlexRequests.Api.Interfaces
int seasonCount, int[] seasons, string apiKey, Uri baseUrl, bool monitor = true, int seasonCount, int[] seasons, string apiKey, Uri baseUrl, bool monitor = true,
bool searchForMissingEpisodes = false); 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); SystemStatus SystemStatus(string apiKey, Uri baseUrl);
List<Series> GetSeries(string apiKey, Uri baseUrl); List<Series> GetSeries(string apiKey, Uri baseUrl);

View file

@ -49,6 +49,7 @@ namespace PlexRequests.Api.Models.Plex
public class User public class User
{ {
public string email { get; set; } public string email { get; set; }
public string uuid { get; set; }
public string joined_at { get; set; } public string joined_at { get; set; }
public string username { get; set; } public string username { get; set; }
public string title { get; set; } public string title { get; set; }

View file

@ -1,7 +1,7 @@
#region Copyright #region Copyright
// /************************************************************************ // /************************************************************************
// Copyright (c) 2016 Jamie Rees // Copyright (c) 2016 Jamie Rees
// File: RecentlyAdded.cs // File: RecentlyAddedModel.cs
// Created By: Jamie Rees // Created By: Jamie Rees
// //
// Permission is hereby granted, free of charge, to any person obtaining // Permission is hereby granted, free of charge, to any person obtaining
@ -32,53 +32,102 @@ namespace PlexRequests.Api.Models.Plex
public class RecentlyAddedChild public class RecentlyAddedChild
{ {
public string _elementType { get; set; } 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 int ratingKey { get; set; }
public string key { get; set; } public string key { get; set; }
public int parentRatingKey { get; set; } public int parentRatingKey { get; set; }
public int grandparentRatingKey { get; set; }
public string type { get; set; } public string type { get; set; }
public string title { get; set; } public string title { get; set; }
public string grandparentKey { get; set; }
public string parentKey { get; set; } public string parentKey { get; set; }
public string parentTitle { get; set; } public string grandparentTitle { get; set; }
public string parentSummary { get; set; }
public string summary { get; set; } public string summary { get; set; }
public int index { get; set; } public int index { get; set; }
public int parentIndex { get; set; } public int parentIndex { get; set; }
public string thumb { get; set; } public string thumb { get; set; }
public string art { get; set; } public string art { get; set; }
public string parentThumb { get; set; } public string grandparentThumb { get; set; }
public int leafCount { get; set; } public string grandparentArt { get; set; }
public int viewedLeafCount { get; set; } public int duration { get; set; }
public int addedAt { get; set; } public int addedAt { get; set; }
public int updatedAt { get; set; } public int updatedAt { get; set; }
public List<object> _children { get; set; } public string chapterSource { get; set; }
public string studio { get; set; } public List<Child2> _children { get; set; }
public string contentRating { get; set; } public string contentRating { get; set; }
public string rating { get; set; } public int? year { get; set; }
public string parentThumb { get; set; }
public string grandparentTheme { get; set; }
public string originallyAvailableAt { get; set; }
public string titleSort { get; set; }
public int? viewCount { get; set; } public int? viewCount { get; set; }
public int? lastViewedAt { get; set; } public int? lastViewedAt { get; set; }
public int? year { get; set; }
public int? duration { 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? viewOffset { 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 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 _elementType { get; set; }
public string allowSync { get; set; } public string allowSync { get; set; }
public string art { get; set; }
public string identifier { 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 mediaTagPrefix { get; set; }
public string mediaTagVersion { get; set; } public string mediaTagVersion { get; set; }
public string mixedParents { 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; } public List<RecentlyAddedChild> _children { get; set; }
} }
} }

View file

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

View file

@ -4,6 +4,10 @@ namespace PlexRequests.Api.Models.Tv
{ {
public class TvMazeShow public class TvMazeShow
{ {
public TvMazeShow()
{
Season = new List<TvMazeCustomSeason>();
}
public int id { get; set; } public int id { get; set; }
public string url { get; set; } public string url { get; set; }
public string name { get; set; } public string name { get; set; }
@ -23,10 +27,16 @@ namespace PlexRequests.Api.Models.Tv
public string summary { get; set; } public string summary { get; set; }
public int updated { get; set; } public int updated { get; set; }
public Links _links { get; set; } public Links _links { get; set; }
public int seasonCount { get; set; } public List<TvMazeCustomSeason> Season { get; set; }
public Embedded _embedded { get; set; } public Embedded _embedded { get; set; }
} }
public class TvMazeCustomSeason
{
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
}
public class Season public class Season
{ {
public int id { get; set; } public int id { get; set; }

View file

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

View file

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

View file

@ -135,6 +135,75 @@ namespace PlexRequests.Api
return result; 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
{
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 SystemStatus SystemStatus(string apiKey, Uri baseUrl) public SystemStatus SystemStatus(string apiKey, Uri baseUrl)
{ {
var request = new RestRequest { Resource = "/api/system/status", Method = Method.GET }; var request = new RestRequest { Resource = "/api/system/status", Method = Method.GET };

View file

@ -93,7 +93,17 @@ namespace PlexRequests.Api
request.AddHeader("Content-Type", "application/json"); request.AddHeader("Content-Type", "application/json");
var obj = Api.Execute<TvMazeShow>(request, new Uri(Uri)); 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; return obj;
} }
@ -110,6 +120,7 @@ namespace PlexRequests.Api
return Api.Execute<List<TvMazeSeasons>>(request, new Uri(Uri)); return Api.Execute<List<TvMazeSeasons>>(request, new Uri(Uri));
} }
public int GetSeasonCount(int id) public int GetSeasonCount(int id)
{ {
var obj = GetSeasons(id); var obj = GetSeasons(id);

View file

@ -6,5 +6,6 @@
<package id="NLog" version="4.3.6" targetFramework="net45" /> <package id="NLog" version="4.3.6" targetFramework="net45" />
<package id="Polly-Signed" version="4.3.0" targetFramework="net45" /> <package id="Polly-Signed" version="4.3.0" targetFramework="net45" />
<package id="RestSharp" version="105.2.3" 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" /> <package id="TMDbLib" version="0.9.0.0-alpha" targetFramework="net45" />
</packages> </packages>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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%"> <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr> <tr>
<td align="center"> <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> </td>
</tr> </tr>
<tr> <tr>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -38,5 +38,13 @@ namespace PlexRequests.Helpers
var tzc = TimeZoneInfo.GetSystemTimeZones(); var tzc = TimeZoneInfo.GetSystemTimeZones();
return tzc.FirstOrDefault(x => x.BaseUtcOffset.TotalMinutes == -minuteOffset); 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;
}
} }
} }

View file

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

View file

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

View file

@ -24,6 +24,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -64,5 +66,36 @@ namespace PlexRequests.Helpers
} }
return sb.ToString(); 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;
}
}
}
} }
} }

View file

@ -4,9 +4,11 @@ namespace PlexRequests.Helpers
{ {
public class UserClaims public class UserClaims
{ {
public const string Admin = "Admin"; // Can do everything including creating new users and editing settings public const string Admin = nameof(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 PowerUser = nameof(PowerUser); // Can only manage the requests, approve etc.
public const string User = "User"; // Can only request 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
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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%"> <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr> <tr>
<td align="center"> <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> </td>
</tr> </tr>
<tr> <tr>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,6 +11,15 @@ CREATE TABLE IF NOT EXISTS Users
UserProperties BLOB 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 CREATE TABLE IF NOT EXISTS GlobalSettings
( (
@ -59,6 +68,12 @@ CREATE TABLE IF NOT EXISTS DBInfo
SchemaVersion INTEGER SchemaVersion INTEGER
); );
CREATE TABLE IF NOT EXISTS VersionInfo
(
Version INTEGER NOT NULL,
Description VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS ScheduledJobs CREATE TABLE IF NOT EXISTS ScheduledJobs
( (
Id INTEGER PRIMARY KEY AUTOINCREMENT, Id INTEGER PRIMARY KEY AUTOINCREMENT,

View file

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

View file

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

View file

@ -24,6 +24,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System;
using Dapper.Contrib.Extensions; using Dapper.Contrib.Extensions;
namespace PlexRequests.Store namespace PlexRequests.Store

View file

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

View file

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

View file

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

View file

@ -2,7 +2,9 @@
$primary-colour-outline: #ff761b; $primary-colour-outline: #ff761b;
$bg-colour: #333333; $bg-colour: #333333;
$bg-colour-disabled: #252424; $bg-colour-disabled: #252424;
$i: !important; $i:
!important
;
.form-control-custom { .form-control-custom {
background-color: $bg-colour $i; background-color: $bg-colour $i;
@ -222,3 +224,7 @@ button.list-group-item:focus {
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after { .bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-bottom: 6px solid $bg-colour $i; border-bottom: 6px solid $bg-colour $i;
} }
#sidebar-wrapper {
background: $bg-colour-disabled;
}

View file

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

View file

@ -1,6 +1,6 @@
(function () { (function () {
var controller = function ($scope, userManagementService) { var controller = function ($scope, userManagementService, moment) {
$scope.user = {}; // The local user $scope.user = {}; // The local user
$scope.users = []; // list of users $scope.users = []; // list of users
@ -9,6 +9,7 @@
$scope.selectedUser = {}; // User on the right side $scope.selectedUser = {}; // User on the right side
$scope.selectedClaims = {}; $scope.selectedClaims = {};
$scope.minDate = "0001-01-01T00:00:00.0000000+00:00";
$scope.sortType = "username"; $scope.sortType = "username";
$scope.sortReverse = false; $scope.sortReverse = false;
@ -20,9 +21,19 @@
errorMessage: "" errorMessage: ""
}; };
var open = false;
// Select a user to populate on the right side // Select a user to populate on the right side
$scope.selectUser = function (id) { $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 // Get all users in the system
@ -51,19 +62,38 @@
return; 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) userManagementService.addUser($scope.user, $scope.selectedClaims)
.then(function (data) { .then(function (data) {
if (data.message) { if (data.message) {
$scope.error.error = true; $scope.error.error = true;
$scope.error.errorMessage = data.message; $scope.error.errorMessage = data.message;
} else { } 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.user = {};
$scope.selectedClaims = {}; $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}', $scope.$watch('claims|filter:{selected:true}',
function (nv) { function (nv) {
$scope.selectedClaims = nv.map(function (claim) { $scope.selectedClaims = nv.map(function (claim) {
@ -74,13 +104,36 @@
$scope.updateUser = function () { $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() { function getBaseUrl() {
return $('#baseUrl').val(); return $('#baseUrl').val();
} }
$scope.formatDate = function (utcDate) {
return moment.utc(utcDate).local().format('lll');
}
// On page load // On page load
$scope.init = function () { $scope.init = function () {
@ -88,7 +141,21 @@
$scope.getClaims(); $scope.getClaims();
return; 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]);
}()); }());

View file

@ -24,10 +24,28 @@
return $http.get('/usermanagement/claims'); 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 { return {
getUsers: getUsers, getUsers: getUsers,
addUser: addUser, addUser: addUser,
getClaims: getClaims getClaims: getClaims,
updateUser: updateUser,
deleteUser: deleteUser
}; };
} }

View file

@ -344,11 +344,95 @@ label {
.img-circle { .img-circle {
border-radius: 50%; } border-radius: 50%; }
.user-management-menu { #wrapper {
border-style: ridge; padding-left: 0;
height: 100%; -webkit-transition: all 0.5s ease;
left: 60%; -moz-transition: all 0.5s ease;
position: absolute; -o-transition: all 0.5s ease;
width: 40%; transition: all 0.5s ease; }
top: 7%; }
#wrapper.toggled {
padding-right: 250px; }
#sidebar-wrapper {
z-index: 1000;
position: fixed;
right: 250px;
width: 0;
height: 100%;
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;
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

View file

@ -434,11 +434,121 @@ $border-radius: 10px;
border-radius: 50%; border-radius: 50%;
} }
.user-management-menu { #wrapper {
border-style: ridge; padding-left: 0;
height: 100%; -webkit-transition: all 0.5s ease;
left: 60%; -moz-transition: all 0.5s ease;
position: absolute; -o-transition: all 0.5s ease;
width: 40%; transition: all 0.5s ease;
top: 7%; }
#wrapper.toggled {
padding-right: 250px;
}
#sidebar-wrapper {
z-index: 1000;
position: fixed;
right: 250px;
width: 0;
height: 100%;
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;
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

View file

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

View file

@ -133,14 +133,6 @@ namespace PlexRequests.UI.Helpers
var assetLocation = GetBaseUrl(); var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation); 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"; var startUrl = $"{content}/Content";
sb.AppendLine($"<script src=\"{startUrl}/angular.min.js\"></script>"); // Load angular first 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()); 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) public static IHtmlString LoadRequestAssets(this HtmlHelpers helper)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
@ -212,12 +218,14 @@ namespace PlexRequests.UI.Helpers
{ {
var assetLocation = GetBaseUrl(); var assetLocation = GetBaseUrl();
var content = GetContentUrl(assetLocation); var content = GetContentUrl(assetLocation);
var sb = new StringBuilder();
var controller = $"<script src=\"{content}/Content/app/userManagement/userManagementController.js?v={Assembly}\" type=\"text/javascript\"></script>"; sb.Append($"<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/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());
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,5 +46,6 @@ namespace PlexRequests.UI.Models
public bool Video { get; set; } public bool Video { get; set; }
public double VoteAverage { get; set; } public double VoteAverage { get; set; }
public int VoteCount { get; set; } public int VoteCount { get; set; }
public bool AlreadyInCp { get; set; }
} }
} }

View file

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

View file

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

View file

@ -63,7 +63,7 @@ using PlexRequests.Store.Models;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers; using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using Quartz;
using Action = PlexRequests.Helpers.Analytics.Action; using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
@ -80,6 +80,7 @@ namespace PlexRequests.UI.Modules
private ISettingsService<PushbulletNotificationSettings> PushbulletService { get; } private ISettingsService<PushbulletNotificationSettings> PushbulletService { get; }
private ISettingsService<PushoverNotificationSettings> PushoverService { get; } private ISettingsService<PushoverNotificationSettings> PushoverService { get; }
private ISettingsService<HeadphonesSettings> HeadphonesService { get; } private ISettingsService<HeadphonesSettings> HeadphonesService { get; }
private ISettingsService<NewletterSettings> NewsLetterService { get; }
private ISettingsService<LogSettings> LogService { get; } private ISettingsService<LogSettings> LogService { get; }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private ISonarrApi SonarrApi { get; } private ISonarrApi SonarrApi { get; }
@ -112,6 +113,7 @@ namespace PlexRequests.UI.Modules
PushbulletApi pbApi, PushbulletApi pbApi,
ICouchPotatoApi cpApi, ICouchPotatoApi cpApi,
ISettingsService<PushoverNotificationSettings> pushoverSettings, ISettingsService<PushoverNotificationSettings> pushoverSettings,
ISettingsService<NewletterSettings> newsletter,
IPushoverApi pushoverApi, IPushoverApi pushoverApi,
IRepository<LogEntity> logsRepo, IRepository<LogEntity> logsRepo,
INotificationService notify, INotificationService notify,
@ -139,6 +141,7 @@ namespace PlexRequests.UI.Modules
PushoverApi = pushoverApi; PushoverApi = pushoverApi;
NotificationService = notify; NotificationService = notify;
HeadphonesService = headphones; HeadphonesService = headphones;
NewsLetterService = newsletter;
LogService = logs; LogService = logs;
Cache = cache; Cache = cache;
SlackSettings = slackSettings; SlackSettings = slackSettings;
@ -200,11 +203,14 @@ namespace PlexRequests.UI.Modules
Get["/headphones"] = _ => Headphones(); Get["/headphones"] = _ => Headphones();
Post["/headphones"] = _ => SaveHeadphones(); Post["/headphones"] = _ => SaveHeadphones();
Get["/newsletter"] = _ => Newsletter();
Post["/newsletter"] = _ => SaveNewsletter();
Post["/createapikey"] = x => CreateApiKey(); Post["/createapikey"] = x => CreateApiKey();
Post["/autoupdate"] = x => AutoUpdate(); Post["/autoupdate"] = x => AutoUpdate();
Post["/testslacknotification", true] = async (x,ct) => await TestSlackNotification(); Post["/testslacknotification", true] = async (x, ct) => await TestSlackNotification();
Get["/slacknotification"] = _ => SlackNotifications(); Get["/slacknotification"] = _ => SlackNotifications();
Post["/slacknotification"] = _ => SaveSlackNotifications(); 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." }); : 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() private Response CreateApiKey()
{ {
this.RequiresClaims(UserClaims.Admin); this.RequiresClaims(UserClaims.Admin);
@ -942,7 +975,8 @@ namespace PlexRequests.UI.Modules
SonarrCacher = s.SonarrCacher, SonarrCacher = s.SonarrCacher,
StoreBackup = s.StoreBackup, StoreBackup = s.StoreBackup,
StoreCleanup = s.StoreCleanup, StoreCleanup = s.StoreCleanup,
JobRecorder = jobsDict JobRecorder = jobsDict,
RecentlyAddedCron = s.RecentlyAddedCron
}; };
return View["SchedulerSettings", model]; return View["SchedulerSettings", model];
} }
@ -953,6 +987,21 @@ namespace PlexRequests.UI.Modules
Analytics.TrackEventAsync(Category.Admin, Action.Update, "Update ScheduledJobs", Username, CookieHelper.GetAnalyticClientId(Cookies)); Analytics.TrackEventAsync(Category.Admin, Action.Update, "Update ScheduledJobs", Username, CookieHelper.GetAnalyticClientId(Cookies));
var settings = this.Bind<ScheduledJobsSettings>(); 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); var result = await ScheduledJobSettings.SaveSettingsAsync(settings);
return Response.AsJson(result return Response.AsJson(result

View file

@ -25,7 +25,8 @@
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System; using System;
using System.IO;
using Mono.Data.Sqlite;
using Nancy; using Nancy;
using Nancy.ModelBinding; using Nancy.ModelBinding;
using Nancy.Security; using Nancy.Security;
@ -35,6 +36,8 @@ using NLog;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers; using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
@ -59,7 +62,7 @@ namespace PlexRequests.UI.Modules
Post["/plex"] = _ => PlexTest(); Post["/plex"] = _ => PlexTest();
Post["/sickrage"] = _ => SickRageTest(); Post["/sickrage"] = _ => SickRageTest();
Post["/headphones"] = _ => HeadphonesTest(); Post["/headphones"] = _ => HeadphonesTest();
Post["/plexdb"] = _ => TestPlexDb();
} }
private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private static readonly Logger Log = LogManager.GetCurrentClassLogger();
@ -223,5 +226,65 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); ; 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 }); ;
}
}
} }
} }

View file

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

View file

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

View file

@ -40,13 +40,15 @@ using Nancy.Security;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class LoginModule : BaseModule 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) : base(pr)
{ {
UserMapper = m; UserMapper = m;
@ -101,6 +103,14 @@ namespace PlexRequests.UI.Modules
{ {
redirect = !string.IsNullOrEmpty(BaseUrl) ? $"/{BaseUrl}/search" : "/search"; 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); return this.LoginAndRedirect(userId.Value, expiry, redirect);
}; };

View file

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

View file

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

View file

@ -189,7 +189,7 @@ namespace PlexRequests.UI.Modules
switch (searchType) switch (searchType)
{ {
case MovieSearchType.Search: case MovieSearchType.Search:
var movies = await MovieApi.SearchMovie(searchTerm); var movies = await MovieApi.SearchMovie(searchTerm).ConfigureAwait(false);
apiMovies = movies.Select(x => apiMovies = movies.Select(x =>
new MovieResult new MovieResult
{ {
@ -232,8 +232,18 @@ namespace PlexRequests.UI.Modules
var plexMovies = Checker.GetPlexMovies(); var plexMovies = Checker.GetPlexMovies();
var settings = await PrService.GetSettingsAsync(); var settings = await PrService.GetSettingsAsync();
var viewMovies = new List<SearchMovieViewModel>(); var viewMovies = new List<SearchMovieViewModel>();
var counter = 0;
foreach (var movie in apiMovies) 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 var viewMovie = new SearchMovieViewModel
{ {
Adult = movie.Adult, Adult = movie.Adult,
@ -252,7 +262,7 @@ namespace PlexRequests.UI.Modules
VoteCount = movie.VoteCount VoteCount = movie.VoteCount
}; };
var canSee = CanUserSeeThisRequest(viewMovie.Id, settings.UsersCanViewOnlyOwnRequests, dbMovies); 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) if (plexMovie != null)
{ {
viewMovie.Available = true; viewMovie.Available = true;
@ -349,8 +359,7 @@ namespace PlexRequests.UI.Modules
providerId = viewT.Id.ToString(); providerId = viewT.Id.ToString();
} }
var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), t.show.name, t.show.premiered?.Substring(0, 4), providerId);
providerId);
if (plexShow != null) if (plexShow != null)
{ {
viewT.Available = true; viewT.Available = true;
@ -435,6 +444,15 @@ namespace PlexRequests.UI.Modules
private async Task<Response> RequestMovie(int movieId) 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(); var settings = await PrService.GetSettingsAsync();
if (!await CheckRequestLimit(settings, RequestType.Movie)) if (!await CheckRequestLimit(settings, RequestType.Movie))
{ {
@ -479,7 +497,7 @@ namespace PlexRequests.UI.Modules
Type = RequestType.Movie, Type = RequestType.Movie,
Overview = movieInfo.Overview, Overview = movieInfo.Overview,
ImdbId = movieInfo.ImdbId, ImdbId = movieInfo.ImdbId,
PosterPath = "https://image.tmdb.org/t/p/w150/" + movieInfo.PosterPath, PosterPath = movieInfo.PosterPath,
Title = movieInfo.Title, Title = movieInfo.Title,
ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue, ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue,
Status = movieInfo.Status, Status = movieInfo.Status,
@ -535,6 +553,15 @@ namespace PlexRequests.UI.Modules
/// <returns></returns> /// <returns></returns>
private async Task<Response> RequestTvShow(int showId, string seasons) 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 // Get the JSON from the request
var req = (Dictionary<string, object>.ValueCollection)Request.Form.Values; var req = (Dictionary<string, object>.ValueCollection)Request.Form.Values;
EpisodeRequestModel episodeModel = null; EpisodeRequestModel episodeModel = null;
@ -590,7 +617,7 @@ namespace PlexRequests.UI.Modules
RequestedUsers = new List<string> { Username }, RequestedUsers = new List<string> { Username },
Issues = IssueState.None, Issues = IssueState.None,
ImdbId = showInfo.externals?.imdb ?? string.Empty, ImdbId = showInfo.externals?.imdb ?? string.Empty,
SeasonCount = showInfo.seasonCount, SeasonCount = showInfo.Season.Count,
TvDbId = showId.ToString() TvDbId = showId.ToString()
}; };
@ -703,7 +730,18 @@ namespace PlexRequests.UI.Modules
} }
else 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}" }); return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} {Resources.UI.Search_AlreadyInPlex}" });
} }
@ -719,7 +757,7 @@ namespace PlexRequests.UI.Modules
{ {
model.Approved = true; model.Approved = true;
var s = await sonarrSettings; var s = await sonarrSettings;
var sender = new TvSender(SonarrApi, SickrageApi); var sender = new TvSenderOld(SonarrApi, SickrageApi); // TODO put back
if (s.Enabled) if (s.Enabled)
{ {
var result = await sender.SendToSonarr(s, model); var result = await sender.SendToSonarr(s, model);

View file

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

View file

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

View file

@ -13,13 +13,15 @@ using PlexRequests.Core;
using PlexRequests.Core.Models; using PlexRequests.Core.Models;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Store;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class UserManagementModule : BaseModule 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 #if !DEBUG
this.RequiresClaims(UserClaims.Admin); this.RequiresClaims(UserClaims.Admin);
@ -27,6 +29,7 @@ namespace PlexRequests.UI.Modules
UserMapper = m; UserMapper = m;
PlexApi = plexApi; PlexApi = plexApi;
PlexSettings = plex; PlexSettings = plex;
UserLoginsRepo = userLogins;
Get["/"] = x => Load(); Get["/"] = x => Load();
@ -35,11 +38,14 @@ namespace PlexRequests.UI.Modules
Get["/local/{id}"] = x => LocalDetails((Guid)x.id); Get["/local/{id}"] = x => LocalDetails((Guid)x.id);
Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id); Get["/plex/{id}", true] = async (x, ct) => await PlexDetails(x.id);
Get["/claims"] = x => GetClaims(); Get["/claims"] = x => GetClaims();
Post["/updateuser"] = x => UpdateUser();
Post["/deleteuser"] = x => DeleteUser();
} }
private ICustomUserMapper UserMapper { get; } private ICustomUserMapper UserMapper { get; }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; } private ISettingsService<PlexSettings> PlexSettings { get; }
private IRepository<UserLogins> UserLoginsRepo { get; }
private Negotiator Load() private Negotiator Load()
{ {
@ -50,22 +56,13 @@ namespace PlexRequests.UI.Modules
{ {
var localUsers = await UserMapper.GetUsersAsync(); var localUsers = await UserMapper.GetUsersAsync();
var model = new List<UserManagementUsersViewModel>(); var model = new List<UserManagementUsersViewModel>();
var usersDb = UserLoginsRepo.GetAll().ToList();
foreach (var user in localUsers) foreach (var user in localUsers)
{ {
var claims = ByteConverterHelper.ReturnObject<string[]>(user.Claims); var userDb = usersDb.FirstOrDefault(x => x.UserId == user.UserGuid);
var claimsString = string.Join(", ", claims); model.Add(MapLocalUser(user, userDb?.LastLoggedIn ?? DateTime.MinValue));
var userProps = ByteConverterHelper.ReturnObject<UserProperties>(user.UserProperties);
model.Add(new UserManagementUsersViewModel
{
Id = user.UserGuid,
Claims = claimsString,
Username = user.UserName,
Type = UserType.LocalUser,
EmailAddress = userProps.EmailAddress,
ClaimsArray = claims
});
} }
var plexSettings = await PlexSettings.GetSettingsAsync(); var plexSettings = await PlexSettings.GetSettingsAsync();
@ -76,7 +73,7 @@ namespace PlexRequests.UI.Modules
foreach (var u in plexUsers.User) foreach (var u in plexUsers.User)
{ {
var userDb = usersDb.FirstOrDefault(x => x.UserId == u.Id);
model.Add(new UserManagementUsersViewModel model.Add(new UserManagementUsersViewModel
{ {
Username = u.Username, Username = u.Username,
@ -87,7 +84,8 @@ namespace PlexRequests.UI.Modules
PlexInfo = new UserManagementPlexInformation PlexInfo = new UserManagementPlexInformation
{ {
Thumb = u.Thumb 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 }); var user = UserMapper.CreateUser(model.Username, model.Password, model.Claims, new UserProperties { EmailAddress = model.EmailAddress });
if (user.HasValue) 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" }); 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) private Response LocalDetails(Guid id)
{ {
var localUser = UserMapper.GetUser(id); var localUser = UserMapper.GetUser(id);
@ -165,6 +231,45 @@ namespace PlexRequests.UI.Modules
} }
return Response.AsJson(retVal); 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;
}
} }
} }

View file

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

View file

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

View file

@ -446,4 +446,25 @@
<data name="Custom_Donation_Default" xml:space="preserve"> <data name="Custom_Donation_Default" xml:space="preserve">
<value>Donate to Library Maintainer</value> <value>Donate to Library Maintainer</value>
</data> </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> </root>

View file

@ -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> /// <summary>
/// Looks up a localized string similar to Coming Soon. /// Looks up a localized string similar to Coming Soon.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to Movies. /// Looks up a localized string similar to Movies.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to We could not remove this notification because you never had it!. /// Looks up a localized string similar to We could not remove this notification because you never had it!.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to Report Issue. /// Looks up a localized string similar to Report Issue.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to Requested. /// Looks up a localized string similar to Requested.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to The request of TV Shows is not correctly set up. Please contact your admin.. /// Looks up a localized string similar to The request of TV Shows is not correctly set up. Please contact your admin..
/// </summary> /// </summary>

View file

@ -32,7 +32,8 @@ using Ninject.Planning.Bindings.Resolvers;
using NLog; using NLog;
using Owin; using Owin;
using PlexRequests.Core.Migration;
using PlexRequests.Services.Jobs;
using PlexRequests.UI.Helpers; using PlexRequests.UI.Helpers;
using PlexRequests.UI.Jobs; using PlexRequests.UI.Jobs;
using PlexRequests.UI.NinjectModules; using PlexRequests.UI.NinjectModules;
@ -62,11 +63,26 @@ namespace PlexRequests.UI
Debug.WriteLine("Added Contravariant Binder"); Debug.WriteLine("Added Contravariant Binder");
kernel.Components.Add<IBindingResolver, ContravariantBindingResolver>(); kernel.Components.Add<IBindingResolver, ContravariantBindingResolver>();
Debug.WriteLine("Start the bootstrapper with the Kernel.ı"); Debug.WriteLine("Start the bootstrapper with the Kernel.");
app.UseNancy(options => options.Bootstrapper = new Bootstrapper(kernel)); app.UseNancy(options => options.Bootstrapper = new Bootstrapper(kernel));
Debug.WriteLine("Finished bootstrapper"); 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(); var scheduler = new Scheduler();
scheduler.StartScheduler(); scheduler.StartScheduler();
//var c = kernel.Get<IRecentlyAdded>();
//c.Test();
} }
catch (Exception exception) catch (Exception exception)
{ {

View file

@ -54,7 +54,7 @@
<p class="form-group">Notice Message</p> <p class="form-group">Notice Message</p>
<div class="form-group"> <div class="form-group">
<div> <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>
</div> </div>
@ -121,6 +121,8 @@
}); });
$('#save').click(function (e) { $('#save').click(function (e) {
e.preventDefault();
var start = ''; var start = '';
var end = ''; var end = '';
if ($startDate.data("DateTimePicker").date()) { if ($startDate.data("DateTimePicker").date()) {
@ -130,8 +132,6 @@
end = $endDate.data("DateTimePicker").date().toISOString(); end = $endDate.data("DateTimePicker").date().toISOString();
} }
e.preventDefault();
var $form = $("#mainForm"); var $form = $("#mainForm");
var data = $form.serialize(); var data = $form.serialize();

View file

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

View file

@ -16,7 +16,7 @@
<form class="form-horizontal" method="POST" id="mainForm"> <form class="form-horizontal" method="POST" id="mainForm">
<fieldset> <fieldset>
<legend>Plex Settings</legend> <legend>Plex Settings</legend>
@*<input id="advancedToggle" type="checkbox"/>*@ @*TODO*@
<div class="form-group"> <div class="form-group">
<label for="Ip" class="control-label">Plex Hostname or IP</label> <label for="Ip" class="control-label">Plex Hostname or IP</label>
<div> <div>
@ -88,6 +88,21 @@
</div> </div>
</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"> <div class="form-group">
<label for="authToken" class="control-label">Plex Authorization Token</label> <label for="authToken" class="control-label">Plex Authorization Token</label>
<div class=""> <div class="">
@ -95,6 +110,8 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="username" class="control-label">Username and Password</label> <label for="username" class="control-label">Username and Password</label>
<div> <div>
@ -129,6 +146,9 @@
<script> <script>
$(function () { $(function () {
$("#advancedToggle").bootstrapSwitch();
var base = '@Html.GetBaseUrl()'; var base = '@Html.GetBaseUrl()';
$('#testPlex').click(function (e) { $('#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) { $('#requestToken').click(function (e) {
e.preventDefault(); e.preventDefault();
var $form = $("#mainForm"); var $form = $("#mainForm");

View file

@ -16,7 +16,7 @@
{ {
<div class="row"> <div class="row">
<div class="col-md-4">@record.Key</div> <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> </div>
<hr style="margin-top: 4px; margin-bottom: 4px"/> <hr style="margin-top: 4px; margin-bottom: 4px"/>
} }
@ -79,10 +79,11 @@
</div> </div>
</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"> <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> <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>
</div> </div>

View file

@ -74,27 +74,6 @@
</select> </select>
</div> </div>
</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="form-group">
<div class="checkbox"> <div class="checkbox">
@ -382,7 +361,7 @@
}); });
}); });
$('#refreshKey').click(function (e) { $('#refreshKey').click(function(e) {
e.preventDefault(); e.preventDefault();
var base = '@Html.GetBaseUrl()'; var base = '@Html.GetBaseUrl()';
var url = createBaseUrl(base, '/admin/createapikey'); var url = createBaseUrl(base, '/admin/createapikey');
@ -391,37 +370,13 @@
type: "post", type: "post",
url: url, url: url,
dataType: "json", dataType: "json",
success: function (response) { success: function(response) {
if (response) { if (response) {
generateNotify("Success!", "success"); generateNotify("Success!", "success");
$('#apiKey').val(response); $('#apiKey').val(response);
} }
}, },
error: function (e) { 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) {
console.log(e); console.log(e);
generateNotify("Something went wrong!", "danger"); generateNotify("Something went wrong!", "danger");
} }

View file

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

View file

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

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