Merge pull request #570 from tidusjar/dev

Dev to master
pull/571/head v1.9.3
Jamie 8 years ago committed by GitHub
commit ce956aaebb

@ -38,6 +38,6 @@ namespace PlexRequests.Api.Interfaces
CouchPotatoProfiles GetProfiles(Uri url, string apiKey); CouchPotatoProfiles GetProfiles(Uri url, string apiKey);
CouchPotatoMovies GetMovies(Uri baseUrl, string apiKey, string[] status); CouchPotatoMovies GetMovies(Uri baseUrl, string apiKey, string[] status);
CoucPotatoApiKey GetApiKey(Uri baseUrl, string username, string password); CouchPotatoApiKey GetApiKey(Uri baseUrl, string username, string password);
} }
} }

@ -44,5 +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);
} }
} }

@ -46,7 +46,10 @@ namespace PlexRequests.Api.Interfaces
IEnumerable<SonarrEpisodes> GetEpisodes(string seriesId, string apiKey, Uri baseUrl); IEnumerable<SonarrEpisodes> GetEpisodes(string seriesId, string apiKey, Uri baseUrl);
SonarrEpisode GetEpisode(string episodeId, string apiKey, Uri baseUrl); SonarrEpisode GetEpisode(string episodeId, string apiKey, Uri baseUrl);
SonarrEpisode UpdateEpisode(SonarrEpisode episodeInfo, string apiKey, Uri baseUrl); SonarrEpisode UpdateEpisode(SonarrEpisode episodeInfo, string apiKey, Uri baseUrl);
SonarrEpisodes UpdateEpisode(SonarrEpisodes episodeInfo, string apiKey, Uri baseUrl);
SonarrAddEpisodeResult SearchForEpisodes(int[] episodeIds, string apiKey, Uri baseUrl); SonarrAddEpisodeResult SearchForEpisodes(int[] episodeIds, string apiKey, Uri baseUrl);
Series UpdateSeries(Series series, string apiKey, Uri baseUrl); Series UpdateSeries(Series series, string apiKey, Uri baseUrl);
SonarrSeasonSearchResult SearchForSeason(int seriesId, int seasonNumber, string apiKey, Uri baseUrl);
SonarrSeriesSearchResult SearchForSeries(int seriesId, string apiKey, Uri baseUrl);
} }
} }

@ -1,7 +1,7 @@
#region Copyright #region Copyright
// /************************************************************************ // /************************************************************************
// Copyright (c) 2016 Jamie Rees // Copyright (c) 2016 Jamie Rees
// File: CoucPotatoApiKey.cs // File: CouchPotatoApiKey.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
@ -28,10 +28,10 @@ using Newtonsoft.Json;
namespace PlexRequests.Api.Models.Movie namespace PlexRequests.Api.Models.Movie
{ {
public class CoucPotatoApiKey public class CouchPotatoApiKey
{ {
[JsonProperty("success")] [JsonProperty("success")]
public bool Result { get; set; } public bool success { get; set; }
[JsonProperty("api_key")] [JsonProperty("api_key")]
public string ApiKey { get; set; } public string ApiKey { get; set; }
} }

@ -0,0 +1,84 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: RecentlyAdded.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
namespace PlexRequests.Api.Models.Plex
{
public class RecentlyAddedChild
{
public string _elementType { get; set; }
public string allowSync { get; set; }
public string librarySectionID { get; set; }
public string librarySectionTitle { get; set; }
public string librarySectionUUID { get; set; }
public int ratingKey { get; set; }
public string key { get; set; }
public int parentRatingKey { get; set; }
public string type { get; set; }
public string title { get; set; }
public string parentKey { get; set; }
public string parentTitle { get; set; }
public string parentSummary { get; set; }
public string summary { get; set; }
public int index { get; set; }
public int parentIndex { get; set; }
public string thumb { get; set; }
public string art { get; set; }
public string parentThumb { get; set; }
public int leafCount { get; set; }
public int viewedLeafCount { get; set; }
public int addedAt { get; set; }
public int updatedAt { get; set; }
public List<object> _children { get; set; }
public string studio { get; set; }
public string contentRating { get; set; }
public string rating { get; set; }
public int? viewCount { get; set; }
public int? lastViewedAt { get; set; }
public int? year { get; set; }
public int? duration { get; set; }
public string 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 string originalTitle { get; set; }
}
public class RecentlyAdded
{
public string _elementType { get; set; }
public string allowSync { get; set; }
public string identifier { get; set; }
public string mediaTagPrefix { get; set; }
public string mediaTagVersion { get; set; }
public string mixedParents { get; set; }
public List<RecentlyAddedChild> _children { get; set; }
}
}

@ -49,7 +49,7 @@
<Compile Include="Movie\CouchPotatoMovies.cs" /> <Compile Include="Movie\CouchPotatoMovies.cs" />
<Compile Include="Movie\CouchPotatoProfiles.cs" /> <Compile Include="Movie\CouchPotatoProfiles.cs" />
<Compile Include="Movie\CouchPotatoStatus.cs" /> <Compile Include="Movie\CouchPotatoStatus.cs" />
<Compile Include="Movie\CoucPotatoApiKey.cs" /> <Compile Include="Movie\CouchPotatoApiKey.cs" />
<Compile Include="Music\HeadphonesAlbumSearchResult.cs" /> <Compile Include="Music\HeadphonesAlbumSearchResult.cs" />
<Compile Include="Music\HeadphonesArtistSearchResult.cs" /> <Compile Include="Music\HeadphonesArtistSearchResult.cs" />
<Compile Include="Music\HeadphonesGetIndex.cs" /> <Compile Include="Music\HeadphonesGetIndex.cs" />
@ -73,6 +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="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" />
@ -89,6 +90,9 @@
<Compile Include="Sonarr\SonarrEpisodes.cs" /> <Compile Include="Sonarr\SonarrEpisodes.cs" />
<Compile Include="Sonarr\SonarrError.cs" /> <Compile Include="Sonarr\SonarrError.cs" />
<Compile Include="Sonarr\SonarrProfile.cs" /> <Compile Include="Sonarr\SonarrProfile.cs" />
<Compile Include="Sonarr\SonarrSearchCommand.cs" />
<Compile Include="Sonarr\SonarrSeasonSearchResult.cs" />
<Compile Include="Sonarr\SonarrSeriesSearchResult.cs" />
<Compile Include="Sonarr\SystemStatus.cs" /> <Compile Include="Sonarr\SystemStatus.cs" />
<Compile Include="Tv\Authentication.cs" /> <Compile Include="Tv\Authentication.cs" />
<Compile Include="Tv\TvMazeEpisodes.cs" /> <Compile Include="Tv\TvMazeEpisodes.cs" />

@ -29,6 +29,6 @@ namespace PlexRequests.Api.Models.Sonarr
public class SonarrAddEpisodeBody public class SonarrAddEpisodeBody
{ {
public string name { get; set; } public string name { get; set; }
public int[] episodeIds { get; set; } public int[] episodeIds { get; set; }
} }
} }

@ -0,0 +1,38 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SonarrSearchCommand.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.Xml.Linq;
namespace PlexRequests.Api.Models.Sonarr
{
public class SonarrSearchCommand
{
public int seriesId { get; set; }
public int seasonNumber { get; set; }
public string name { get; set; }
}
}

@ -0,0 +1,55 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SonarrSeasonSearchResult.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.Api.Models.Sonarr
{
public class SeasonBody
{
public int seriesId { get; set; }
public int seasonNumber { get; set; }
public bool sendUpdatesToClient { get; set; }
public bool updateScheduledTask { get; set; }
public string completionMessage { get; set; }
public string name { get; set; }
public string trigger { get; set; }
}
public class SonarrSeasonSearchResult
{
public string name { get; set; }
public SeasonBody body { get; set; }
public string priority { get; set; }
public string status { get; set; }
public string queued { get; set; }
public string trigger { get; set; }
public string state { get; set; }
public bool manual { get; set; }
public string startedOn { get; set; }
public bool sendUpdatesToClient { get; set; }
public bool updateScheduledTask { get; set; }
public int id { get; set; }
}
}

@ -0,0 +1,56 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SonarrSeriesSearchResult.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.Api.Models.Sonarr
{
public class SeriesBody
{
public int seriesId { get; set; }
public bool sendUpdatesToClient { get; set; }
public bool updateScheduledTask { get; set; }
public string completionMessage { get; set; }
public string name { get; set; }
public string trigger { get; set; }
}
public class SonarrSeriesSearchResult
{
public string name { get; set; }
public SeriesBody body { get; set; }
public string priority { get; set; }
public string status { get; set; }
public string queued { get; set; }
public string started { get; set; }
public string trigger { get; set; }
public string state { get; set; }
public bool manual { get; set; }
public string startedOn { get; set; }
public string stateChangeTime { get; set; }
public bool sendUpdatesToClient { get; set; }
public bool updateScheduledTask { get; set; }
public int id { get; set; }
}
}

@ -160,18 +160,18 @@ namespace PlexRequests.Api
} }
} }
public CoucPotatoApiKey GetApiKey(Uri baseUrl, string username, string password) public CouchPotatoApiKey GetApiKey(Uri baseUrl, string username, string password)
{ {
var request = new RestRequest var request = new RestRequest
{ {
Resource = "getkey/?p={username}&u={password}", Resource = "getkey/?u={username}&p={password}",
Method = Method.GET Method = Method.GET
}; };
request.AddUrlSegment("username", StringHasher.CalcuateMd5Hash(username)); request.AddUrlSegment("username", StringHasher.CalcuateMd5Hash(username));
request.AddUrlSegment("password", StringHasher.CalcuateMd5Hash(password)); request.AddUrlSegment("password", StringHasher.CalcuateMd5Hash(password));
var obj = RetryHandler.Execute(() => Api.Execute<CoucPotatoApiKey>(request, baseUrl), var obj = RetryHandler.Execute(() => Api.Execute<CouchPotatoApiKey>(request, baseUrl),
(exception, timespan) => Log.Error(exception, "Exception when calling GetApiKey for CP, Retrying {0}", timespan), null); (exception, timespan) => Log.Error(exception, "Exception when calling GetApiKey for CP, Retrying {0}", timespan), null);
return obj; return obj;

@ -347,6 +347,39 @@ namespace PlexRequests.Api
return servers; return servers;
} }
public RecentlyAdded RecentlyAdded(string authToken, Uri plexFullHost)
{
var request = new RestRequest
{
Method = Method.GET,
Resource = "library/recentlyAdded"
};
request.AddHeader("X-Plex-Token", authToken);
request.AddHeader("X-Plex-Client-Identifier", $"PlexRequests.Net{Version}");
request.AddHeader("X-Plex-Product", "Plex Requests .Net");
request.AddHeader("X-Plex-Version", Version);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
try
{
var lib = RetryHandler.Execute(() => Api.ExecuteJson<RecentlyAdded>(request, plexFullHost),
(exception, timespan) => Log.Error(exception, "Exception when calling RecentlyAdded for Plex, Retrying {0}", timespan), new[] {
TimeSpan.FromSeconds (5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
});
return lib;
}
catch (Exception e)
{
Log.Error(e, "There has been a API Exception when attempting to get the Plex RecentlyAdded");
return new RecentlyAdded();
}
}
private void AddHeaders(ref RestRequest request, string authToken) private void AddHeaders(ref RestRequest request, string authToken)
{ {
request.AddHeader("X-Plex-Token", authToken); request.AddHeader("X-Plex-Token", authToken);

@ -273,6 +273,25 @@ namespace PlexRequests.Api
} }
} }
public SonarrEpisodes UpdateEpisode(SonarrEpisodes episodeInfo, string apiKey, Uri baseUrl)
{
var request = new RestRequest { Resource = "/api/Episode", Method = Method.PUT };
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(episodeInfo);
try
{
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) =>
Log.Error(exception, "Exception when calling UpdateEpisode for Sonarr, Retrying {0}", timespan));
return policy.Execute(() => Api.ExecuteJson<SonarrEpisodes>(request, baseUrl));
}
catch (Exception e)
{
Log.Error(e, "There has been an API exception when put the Sonarr UpdateEpisode");
return null;
}
}
/// <summary> /// <summary>
/// Search for one or more episodes /// Search for one or more episodes
/// </summary> /// </summary>
@ -327,5 +346,58 @@ namespace PlexRequests.Api
return null; return null;
} }
} }
public SonarrSeasonSearchResult SearchForSeason(int seriesId, int seasonNumber, string apiKey, Uri baseUrl)
{
var request = new RestRequest { Resource = "/api/Command", Method = Method.POST };
request.AddHeader("X-Api-Key", apiKey);
var body = new SonarrSearchCommand
{
name = "SeasonSearch",
seriesId = seriesId,
seasonNumber = seasonNumber
};
request.AddJsonBody(body);
try
{
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) =>
Log.Error(exception, "Exception when calling SearchForSeason for Sonarr, Retrying {0}", timespan));
return policy.Execute(() => Api.ExecuteJson<SonarrSeasonSearchResult>(request, baseUrl));
}
catch (Exception e)
{
Log.Error(e, "There has been an API exception when put the Sonarr SearchForSeason");
return null;
}
}
public SonarrSeriesSearchResult SearchForSeries(int seriesId, string apiKey, Uri baseUrl)
{
var request = new RestRequest { Resource = "/api/Command", Method = Method.POST };
request.AddHeader("X-Api-Key", apiKey);
var body = new SonarrSearchCommand
{
name = "SeriesSearch",
seriesId = seriesId
};
request.AddJsonBody(body);
try
{
var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) =>
Log.Error(exception, "Exception when calling SearchForSeries for Sonarr, Retrying {0}", timespan));
return policy.Execute(() => Api.ExecuteJson<SonarrSeriesSearchResult>(request, baseUrl));
}
catch (Exception e)
{
Log.Error(e, "There has been an API exception when put the Sonarr SearchForSeries");
return null;
}
}
} }
} }

@ -74,6 +74,12 @@ namespace PlexRequests.Api
return movies; return movies;
} }
public async Task<Movie> GetMovieInformation(string imdbId)
{
var movies = await Client.GetMovie(imdbId);
return movies;
}
[Obsolete("Should use TvMaze for TV")] [Obsolete("Should use TvMaze for TV")]
public async Task<TvShow> GetTvShowInformation(int tmdbId) public async Task<TvShow> GetTvShowInformation(int tmdbId)
{ {

@ -1,147 +1,147 @@
#region Copyright //#region Copyright
// /************************************************************************ //// /************************************************************************
// Copyright (c) 2016 Jamie Rees //// Copyright (c) 2016 Jamie Rees
// File: AuthenticationSettingsTests.cs //// File: AuthenticationSettingsTests.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
// a copy of this software and associated documentation files (the //// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including //// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish, //// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to //// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to //// permit persons to whom the Software is furnished to do so, subject to
// the following conditions: //// the following conditions:
// ////
// The above copyright notice and this permission notice shall be //// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software. //// included in all copies or substantial portions of the Software.
// ////
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, //// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF //// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND //// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE //// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION //// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// 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;
using System.Collections.Generic; //using System.Collections.Generic;
using NUnit.Framework; //using NUnit.Framework;
using PlexRequests.Core.Models; //using PlexRequests.Core.Models;
using PlexRequests.Core.Notification; //using PlexRequests.Core.Notification;
using PlexRequests.Core.SettingModels; //using PlexRequests.Core.SettingModels;
namespace PlexRequests.Core.Tests //namespace PlexRequests.Core.Tests
{ //{
[TestFixture] // [TestFixture]
public class NotificationMessageResolverTests // public class NotificationMessageResolverTests
{ // {
[TestCaseSource(nameof(MessageBodyResolver))] // [TestCaseSource(nameof(MessageBodyResolver))]
public string ResolveBody(string body, NotificationMessageCurlys param) // public string ResolveBody(string body, NotificationMessageCurlys param)
{ // {
var n = new NotificationMessageResolver(); // var n = new NotificationMessageResolver();
var s = new NotificationSettings // var s = new NotificationSettings
{ // {
Message = new List<Notification.NotificationMessage> { new Notification.NotificationMessage { NotificationType = NotificationType.NewRequest, Body = body } } // Message = new List<Notification.NotificationMessage> { new Notification.NotificationMessage { NotificationType = NotificationType.NewRequest, Body = body } }
}; // };
var result = n.ParseMessage(s, NotificationType.NewRequest, param); // var result = n.ParseMessage(s, NotificationType.NewRequest, param, TransportType.Email);
return result.Body; // return result.Body;
} // }
[TestCaseSource(nameof(MessageSubjectResolver))] // [TestCaseSource(nameof(MessageSubjectResolver))]
public string ResolveSubject(string subject, NotificationMessageCurlys param) // public string ResolveSubject(string subject, NotificationMessageCurlys param)
{ // {
var n = new NotificationMessageResolver(); // var n = new NotificationMessageResolver();
var s = new NotificationSettings // var s = new NotificationSettings
{ // {
Message = new List<Notification.NotificationMessage> { new Notification.NotificationMessage { NotificationType = NotificationType.NewRequest, Subject = subject }} // Message = new List<Notification.NotificationMessage> { new Notification.NotificationMessage { NotificationType = NotificationType.NewRequest, Subject = subject }}
}; // };
var result = n.ParseMessage(s, NotificationType.NewRequest, param); // var result = n.ParseMessage(s, NotificationType.NewRequest, param, TransportType.Email);
return result.Subject; // return result.Subject;
} // }
private static IEnumerable<TestCaseData> MessageSubjectResolver // private static IEnumerable<TestCaseData> MessageSubjectResolver
{ // {
get // get
{ // {
yield return new TestCaseData( // yield return new TestCaseData(
"{Username} has requested a {Type}", // "{Username} has requested a {Type}",
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty)) // new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
.Returns("Jamie has requested a Movie").SetName("Subject Curlys"); // .Returns("Jamie has requested a Movie").SetName("Subject Curlys");
yield return new TestCaseData( // yield return new TestCaseData(
null, // null,
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty)) // new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
.Returns(string.Empty).SetName("Empty Subject"); // .Returns(string.Empty).SetName("Empty Subject");
yield return new TestCaseData( // yield return new TestCaseData(
"New Request Incoming!", // "New Request Incoming!",
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty)) // new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
.Returns("New Request Incoming!").SetName("No curlys"); // .Returns("New Request Incoming!").SetName("No curlys");
yield return new TestCaseData( // yield return new TestCaseData(
"%$R£%$£^%$&{Username}@{}:§", // "%$R£%$£^%$&{Username}@{}:§",
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty)) // new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
.Returns("%$R£%$£^%$&Jamie@{}:§").SetName("Special Chars"); // .Returns("%$R£%$£^%$&Jamie@{}:§").SetName("Special Chars");
} // }
} // }
private static IEnumerable<TestCaseData> MessageBodyResolver // private static IEnumerable<TestCaseData> MessageBodyResolver
{ // {
get // get
{ // {
yield return new TestCaseData( // yield return new TestCaseData(
"There has been a new request from {Username}, Title: {Title} for {Type}", // "There has been a new request from {Username}, Title: {Title} for {Type}",
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty)) // new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
.Returns("There has been a new request from Jamie, Title: Finding Dory for Movie").SetName("FindingDory"); // .Returns("There has been a new request from Jamie, Title: Finding Dory for Movie").SetName("FindingDory");
yield return new TestCaseData( // yield return new TestCaseData(
null, // null,
new NotificationMessageCurlys(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty)) // new NotificationMessageCurlys(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty))
.Returns(string.Empty) // .Returns(string.Empty)
.SetName("Empty Message"); // .SetName("Empty Message");
yield return new TestCaseData( // yield return new TestCaseData(
"{{Wowwzer}} Damn}{{Username}}}}", // "{{Wowwzer}} Damn}{{Username}}}}",
new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, string.Empty)) // new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, string.Empty))
.Returns("{{Wowwzer}} Damn}{HEY!}}}") // .Returns("{{Wowwzer}} Damn}{HEY!}}}")
.SetName("Multiple Curlys"); // .SetName("Multiple Curlys");
yield return new TestCaseData( // yield return new TestCaseData(
"This is a message with no curlys", // "This is a message with no curlys",
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty)) // new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
.Returns("This is a message with no curlys") // .Returns("This is a message with no curlys")
.SetName("No Curlys"); // .SetName("No Curlys");
yield return new TestCaseData( // yield return new TestCaseData(
new string(')', 5000), // new string(')', 5000),
new NotificationMessageCurlys(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty)) // new NotificationMessageCurlys(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty))
.Returns(new string(')', 5000)) // .Returns(new string(')', 5000))
.SetName("Long String"); // .SetName("Long String");
yield return new TestCaseData( // yield return new TestCaseData(
"This is a {Username} and {Username} Because {Issue}{Issue}", // "This is a {Username} and {Username} Because {Issue}{Issue}",
new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, "Bob")) // new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, "Bob"))
.Returns("This is a HEY! and HEY! Because BobBob") // .Returns("This is a HEY! and HEY! Because BobBob")
.SetName("Double Curly"); // .SetName("Double Curly");
yield return new TestCaseData( // yield return new TestCaseData(
"This is a {username} and {username} Because {Issue}{Issue}", // "This is a {username} and {username} Because {Issue}{Issue}",
new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, "Bob")) // new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, "Bob"))
.Returns("This is a {username} and {username} Because BobBob") // .Returns("This is a {username} and {username} Because BobBob")
.SetName("Case sensitive"); // .SetName("Case sensitive");
yield return new TestCaseData( // yield return new TestCaseData(
"{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}", // "{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}",
new NotificationMessageCurlys("HEY!", string.Empty, "b", string.Empty, "Bob")) // new NotificationMessageCurlys("HEY!", string.Empty, "b", string.Empty, "Bob"))
.Returns("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") // .Returns("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
.SetName("Lots of curlys"); // .SetName("Lots of curlys");
} // }
} // }
} // }
} //}

@ -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 System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -50,16 +52,37 @@ namespace PlexRequests.Core.Notification
/// <param name="notification">The notification.</param> /// <param name="notification">The notification.</param>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <param name="c">The c.</param> /// <param name="c">The c.</param>
/// <param name="transportType">Type of the transport.</param>
/// <returns></returns> /// <returns></returns>
public NotificationMessageContent ParseMessage<T>(T notification, NotificationType type, NotificationMessageCurlys c) where T : NotificationSettings public NotificationMessageContent ParseMessage(NotificationSettingsV2 notification, NotificationType type, NotificationMessageCurlys c, TransportType transportType)
{ {
var content = notification.Message.FirstOrDefault(x => x.NotificationType == type); IEnumerable<NotificationMessage> content = null;
switch (transportType)
{
case TransportType.Email:
content = notification.EmailNotification;
break;
case TransportType.Pushbullet:
content = notification.PushbulletNotification;
break;
case TransportType.Pushover:
content = notification.PushoverNotification;
break;
case TransportType.Slack:
content = notification.SlackNotification;
break;
default:
throw new ArgumentOutOfRangeException(nameof(transportType), transportType, null);
}
if (content == null) if (content == null)
{ {
return new NotificationMessageContent(); return new NotificationMessageContent();
} }
return Resolve(content.Body, content.Subject, c.Curlys); var message = content.FirstOrDefault(x => x.NotificationType == type) ?? new NotificationMessage();
return Resolve(message.Body, message.Subject, c.Curlys);
} }
/// <summary> /// <summary>
@ -78,7 +101,7 @@ namespace PlexRequests.Core.Notification
body = ReplaceFields(bodyFields, parameters, body); body = ReplaceFields(bodyFields, parameters, body);
subject = ReplaceFields(subjectFields, parameters, subject); subject = ReplaceFields(subjectFields, parameters, subject);
return new NotificationMessageContent { Body = body ?? string.Empty, Subject = subject ?? string.Empty }; return new NotificationMessageContent {Body = body ?? string.Empty, Subject = subject ?? string.Empty};
} }
/// <summary> /// <summary>
@ -123,7 +146,6 @@ namespace PlexRequests.Core.Notification
{ {
currentWord += c.ToString(); // Add the character onto the word. currentWord += c.ToString(); // Add the character onto the word.
} }
} }
return fields; return fields;

@ -0,0 +1,189 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Plex Requests .Net</title>
<style media="all" type="text/css">
@media all {
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
@media all {
.btn-secondary a:hover {
border-color: #34495e !important;
color: #34495e !important;
}
}
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] h2 {
font-size: 22px !important;
margin-bottom: 10px !important;
}
table[class=body] h3 {
font-size: 16px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .header {
margin-bottom: 10px !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
table[class=body] .alert td {
border-radius: 0 !important;
padding: 10px !important;
}
table[class=body] .span-2,
table[class=body] .span-3 {
max-width: none !important;
width: 100% !important;
}
table[class=body] .receipt {
width: 100% !important;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
}
</style>
</head>
<body class="" style="font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; background-color: #f6f6f6; margin: 0; padding: 0;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;" width="100%" bgcolor="#f6f6f6">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto !important; max-width: 580px; padding: 10px; width: 580px;" width="580" valign="top">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Plex Requests</span>
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #fff; border-radius: 3px;" width="100%">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr>
<td align="center">
<img src="http://i.imgur.com/s4nswSA.png?" width="400px" text-align="center" />
</td>
</tr>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi there!</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{@SUBJECT}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{@BODY}</p>
</td>
</tr>
<tr>
<td align="center">
<img src="{@IMGSRC}" width="400px" text-align="center" />
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; padding-top: 10px; text-align: center; 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>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-top: 10px; padding-bottom: 10px; font-size: 12px; color: #999999; text-align: center;" valign="top" align="center">
Powered by <a href="https://github.com/tidusjar/PlexRequests.Net" style="color: #999999; font-size: 12px; text-align: center; text-decoration: underline;">Plex Requests .Net</a>
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
</tr>
</table>
</body>
</html>

@ -0,0 +1,70 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: EmailBasicTemplate.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using NLog;
using PlexRequests.Core.Models;
using PlexRequests.Core.SettingModels;
namespace PlexRequests.Core.Notification.Templates
{
public class EmailBasicTemplate : IEmailBasicTemplate
{
public string TemplateLocation => Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, "Notification", "Templates", "BasicRequestTemplate.html");
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private const string SubjectKey = "{@SUBJECT}";
private const string BodyKey = "{@BODY}";
private const string ImgSrc = "{@IMGSRC}";
private const string DateKey = "{@DATENOW}";
public string LoadTemplate(string subject, string body, string imgSrc)
{
try
{
var sb = new StringBuilder(File.ReadAllText(TemplateLocation));
sb.Replace(SubjectKey, subject);
sb.Replace(BodyKey, body);
sb.Replace(ImgSrc, imgSrc);
sb.Replace(DateKey, DateTime.Now.ToString("f"));
return sb.ToString();
}
catch (Exception e)
{
Log.Error(e);
return string.Empty;
}
}
}
}

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

@ -0,0 +1,36 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: TransportType.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.Notification
{
public enum TransportType
{
Email,
Pushbullet,
Pushover,
Slack
}
}

@ -44,6 +44,7 @@
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
@ -81,11 +82,15 @@
<Compile Include="Models\NotificationType.cs" /> <Compile Include="Models\NotificationType.cs" />
<Compile Include="Models\StatusModel.cs" /> <Compile Include="Models\StatusModel.cs" />
<Compile Include="Models\UserProperties.cs" /> <Compile Include="Models\UserProperties.cs" />
<Compile Include="Notification\Templates\EmailBasicTemplate.cs" />
<Compile Include="Notification\Templates\IEmailBasicTemplate.cs" />
<Compile Include="Notification\TransportType.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\NotificationSettings.cs" /> <Compile Include="SettingModels\NotificationSettings.cs" />
<Compile Include="SettingModels\NotificationSettingsV2.cs" />
<Compile Include="SettingModels\RequestSettings.cs" /> <Compile Include="SettingModels\RequestSettings.cs" />
<Compile Include="SettingModels\ScheduledJobsSettings.cs" /> <Compile Include="SettingModels\ScheduledJobsSettings.cs" />
<Compile Include="SettingModels\SlackNotificationSettings.cs" /> <Compile Include="SettingModels\SlackNotificationSettings.cs" />
@ -134,7 +139,11 @@
<Name>PlexRequests.Store</Name> <Name>PlexRequests.Store</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup>
<Content Include="Notification\Templates\BasicRequestTemplate.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</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.

@ -34,7 +34,7 @@ namespace PlexRequests.Core.SettingModels
public string EmailSender { get; set; } public string EmailSender { get; set; }
public string EmailUsername { get; set; } public string EmailUsername { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
public bool Authentication { get; set; } public bool Authentication { get; set; }
public bool EnableUserEmailNotifications { get; set; } public bool EnableUserEmailNotifications { get; set; }
public string RecipientEmail { get; set; } public string RecipientEmail { get; set; }
} }

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

@ -56,6 +56,9 @@ namespace PlexRequests.Core.SettingModels
public bool CollectAnalyticData { get; set; } public bool CollectAnalyticData { get; set; }
public bool IgnoreNotifyForAutoApprovedRequests { get; set; } public bool IgnoreNotifyForAutoApprovedRequests { get; set; }
public bool Wizard { get; set; } public bool Wizard { get; set; }
public bool DisableTvRequestsByEpisode { get; set; }
public bool DisableTvRequestsBySeason { get; set; }
public bool SendRecentlyAddedEmail { get; set; }
/// <summary> /// <summary>
/// The CSS name of the theme we want /// The CSS name of the theme we want

@ -38,6 +38,7 @@ namespace PlexRequests.Core.SettingModels
StoreCleanup = 24; StoreCleanup = 24;
UserRequestLimitResetter = 12; UserRequestLimitResetter = 12;
PlexEpisodeCacher = 12; PlexEpisodeCacher = 12;
RecentlyAdded = 168;
} }
public int PlexAvailabilityChecker { get; set; } public int PlexAvailabilityChecker { get; set; }
@ -48,5 +49,6 @@ 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; }
public int RecentlyAdded { get; set; }
} }
} }

@ -1,8 +1,11 @@
namespace PlexRequests.Services.Interfaces using System.Collections.Generic;
{ using PlexRequests.Services.Models;
public interface ISonarrCacher
{ namespace PlexRequests.Services.Interfaces
void Queued(); {
int[] QueuedIds(); public interface ISonarrCacher
} {
} void Queued();
IEnumerable<SonarrCachedResult> QueuedIds();
}
}

@ -0,0 +1,10 @@
using Quartz;
namespace PlexRequests.Services.Jobs
{
public interface IRecentlyAdded
{
void Execute(IJobExecutionContext context);
void Test();
}
}

@ -36,5 +36,6 @@ namespace PlexRequests.Services.Jobs
public const string StoreCleanup = "Database Cleanup"; public const string StoreCleanup = "Database Cleanup";
public const string RequestLimitReset = "Request Limit Reset"; public const string RequestLimitReset = "Request Limit Reset";
public const string EpisodeCacher = "Plex Episode Cacher"; public const string EpisodeCacher = "Plex Episode Cacher";
public const string RecentlyAddedEmail = "Recently Added Email Notification";
} }
} }

@ -0,0 +1,255 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: RecentlyAdded.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.Text;
using MailKit.Net.Smtp;
using MimeKit;
using NLog;
using PlexRequests.Api;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs.Templates;
using Quartz;
namespace PlexRequests.Services.Jobs
{
public class RecentlyAdded : IJob, IRecentlyAdded
{
public RecentlyAdded(IPlexApi api, ISettingsService<PlexSettings> plexSettings, ISettingsService<EmailNotificationSettings> email,
ISettingsService<ScheduledJobsSettings> scheduledService, IJobRecord rec)
{
JobRecord = rec;
Api = api;
PlexSettings = plexSettings;
EmailSettings = email;
ScheduledJobsSettings = scheduledService;
}
private IPlexApi Api { get; }
private TvMazeApi TvApi = new TvMazeApi();
private readonly TheMovieDbApi _movieApi = new TheMovieDbApi();
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<EmailNotificationSettings> EmailSettings { get; }
private ISettingsService<ScheduledJobsSettings> ScheduledJobsSettings { get; }
private IJobRecord JobRecord { get; }
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public void Execute(IJobExecutionContext context)
{
try
{
var jobs = JobRecord.GetJobs();
var thisJob =
jobs.FirstOrDefault(
x => x.Name.Equals(JobNames.RecentlyAddedEmail, StringComparison.CurrentCultureIgnoreCase));
var settings = ScheduledJobsSettings.GetSettings();
if (thisJob?.LastRun > DateTime.Now.AddHours(-settings.RecentlyAdded))
{
return;
}
Start();
}
catch (Exception e)
{
Log.Error(e);
}
finally
{
JobRecord.Record(JobNames.RecentlyAddedEmail);
}
}
public void Test()
{
Start(true);
}
private void Start(bool testEmail = false)
{
var sb = new StringBuilder();
var plexSettings = PlexSettings.GetSettings();
var recentlyAdded = Api.RecentlyAdded(plexSettings.PlexAuthToken, plexSettings.FullUri);
var movies =
recentlyAdded._children.Where(x => x.type.Equals("Movie", StringComparison.CurrentCultureIgnoreCase));
var tv =
recentlyAdded._children.Where(
x => x.type.Equals("season", StringComparison.CurrentCultureIgnoreCase))
.GroupBy(x => x.parentTitle)
.Select(x => x.FirstOrDefault());
GenerateMovieHtml(movies, plexSettings, ref sb);
GenerateTvHtml(tv, plexSettings, ref sb);
var template = new RecentlyAddedTemplate();
var html = template.LoadTemplate(sb.ToString());
Send(html, plexSettings, testEmail);
}
private void GenerateMovieHtml(IEnumerable<RecentlyAddedChild> movies, PlexSettings plexSettings, ref StringBuilder sb)
{
sb.Append("<h1>New Movies:</h1><br/><br/>");
sb.Append("<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var movie in movies)
{
var metaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
movie.ratingKey.ToString());
var imdbId = PlexHelper.GetProviderIdFromPlexGuid(metaData.Video.Guid);
var info = _movieApi.GetMovieInformation(imdbId).Result;
sb.Append("<tr>");
sb.Append("<td align=\"center\">");
sb.AppendFormat("<img src=\"https://image.tmdb.org/t/p/w500{0}\" width=\"400px\" text-align=\"center\" />", info.BackdropPath);
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("<tr>");
sb.Append("<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
sb.AppendFormat("<a href=\"https://www.imdb.com/title/{0}/\"><h3 style=\"font-family: sans-serif; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{1} {2}</p></a>",
info.ImdbId, info.Title, info.ReleaseDate?.ToString("yyyy") ?? string.Empty);
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">Genre: {0}</p>", string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray()));
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{0}</p>", info.Overview);
sb.Append("<td");
sb.Append("</tr>");
sb.Append("<hr>");
sb.Append("<br>");
sb.Append("<br>");
}
sb.Append("</table><br/><br/>");
}
private void GenerateTvHtml(IEnumerable<RecentlyAddedChild> tv, PlexSettings plexSettings, ref StringBuilder sb)
{
// TV
sb.Append("<h1>New Episodes:</h1><br/><br/>");
sb.Append("<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var t in tv)
{
var parentMetaData = Api.GetMetadata(plexSettings.PlexAuthToken, plexSettings.FullUri,
t.parentRatingKey.ToString());
var info = TvApi.ShowLookupByTheTvDbId(int.Parse(PlexHelper.GetProviderIdFromPlexGuid(parentMetaData.Directory.Guid)));
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
}
sb.Append("<tr>");
sb.Append("<td align=\"center\">");
sb.AppendFormat("<img src=\"{0}\" width=\"400px\" text-align=\"center\" />", banner);
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("<tr>");
sb.Append("<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
sb.AppendFormat("<a href=\"https://www.imdb.com/title/{0}/\"><h3 style=\"font-family: sans-serif; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{1} {2}</p></a>",
info.externals.imdb, info.name, info.premiered.Substring(0, 4)); // Only the year
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">Genre: {0}</p>", string.Join(", ", info.genres.Select(x => x.ToString()).ToArray()));
sb.AppendFormat("<p style=\"font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;\">{0}</p>",
string.IsNullOrEmpty(parentMetaData.Directory.Summary) ? info.summary : parentMetaData.Directory.Summary); // Episode Summary
sb.Append("<td");
sb.Append("</tr>");
sb.Append("<hr>");
sb.Append("<br>");
sb.Append("<br>");
}
sb.Append("</table><br/><br/>");
}
private void Send(string html, PlexSettings plexSettings, bool testEmail = false)
{
var settings = EmailSettings.GetSettings();
if (!settings.Enabled || string.IsNullOrEmpty(settings.EmailHost))
{
return;
}
var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." };
var message = new MimeMessage
{
Body = body.ToMessageBody(),
Subject = "New Content on Plex!",
};
if (!testEmail)
{
var users = Api.GetUsers(plexSettings.PlexAuthToken);
foreach (var user in users.User)
{
message.Bcc.Add(new MailboxAddress(user.Username, user.Email));
}
}
message.Bcc.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender)); // Include the admin
message.From.Add(new MailboxAddress(settings.EmailUsername, settings.EmailSender));
try
{
using (var client = new SmtpClient())
{
client.Connect(settings.EmailHost, settings.EmailPort); // Let MailKit figure out the correct SecureSocketOptions.
// Note: since we don't have an OAuth2 token, disable
// the XOAUTH2 authentication mechanism.
client.AuthenticationMechanisms.Remove("XOAUTH2");
if (settings.Authentication)
{
client.Authenticate(settings.EmailUsername, settings.EmailPassword);
}
Log.Info("sending message to {0} \r\n from: {1}\r\n Are we authenticated: {2}", message.To, message.From, client.IsAuthenticated);
client.Send(message);
client.Disconnect(true);
}
}
catch (Exception e)
{
Log.Error(e);
}
}
}
}

@ -35,6 +35,7 @@ using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Models;
using PlexRequests.Store.Models; using PlexRequests.Store.Models;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
@ -84,10 +85,29 @@ namespace PlexRequests.Services.Jobs
} }
// we do not want to set here... // we do not want to set here...
public int[] QueuedIds() public IEnumerable<SonarrCachedResult> QueuedIds()
{ {
var result = new List<SonarrCachedResult>();
var series = Cache.Get<List<Series>>(CacheKeys.SonarrQueued); var series = Cache.Get<List<Series>>(CacheKeys.SonarrQueued);
return series?.Select(x => x.tvdbId).ToArray() ?? new int[] { }; if (series != null)
{
foreach (var s in series)
{
var cached = new SonarrCachedResult {TvdbId = s.tvdbId};
foreach (var season in s.seasons)
{
cached.Seasons.Add(new SonarrSeasons
{
SeasonNumber = season.seasonNumber,
Monitored = season.monitored
});
}
result.Add(cached);
}
}
return result;
} }
public void Execute(IJobExecutionContext context) public void Execute(IJobExecutionContext context)

@ -0,0 +1,58 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: RecentlyAddedTemplate.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.IO;
using System.Text;
using System.Windows.Forms;
using NLog;
namespace PlexRequests.Services.Jobs.Templates
{
public class RecentlyAddedTemplate
{
public string TemplateLocation => Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, "Jobs", "Templates", "RecentlyAddedTemplate.html");
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private const string RecentlyAddedKey = "{@RECENTLYADDED}";
public string LoadTemplate(string html)
{
try
{
var sb = new StringBuilder(File.ReadAllText(TemplateLocation));
sb.Replace(RecentlyAddedKey, html);
return sb.ToString();
}
catch (Exception e)
{
Log.Error(e);
return string.Empty;
}
}
}
}

@ -0,0 +1,187 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Plex Requests .Net</title>
<style media="all" type="text/css">
@media all {
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
@media all {
.btn-secondary a:hover {
border-color: #34495e !important;
color: #34495e !important;
}
}
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] h2 {
font-size: 22px !important;
margin-bottom: 10px !important;
}
table[class=body] h3 {
font-size: 16px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .header {
margin-bottom: 10px !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
table[class=body] .alert td {
border-radius: 0 !important;
padding: 10px !important;
}
table[class=body] .span-2,
table[class=body] .span-3 {
max-width: none !important;
width: 100% !important;
}
table[class=body] .receipt {
width: 100% !important;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
}
</style>
</head>
<body class="" style="font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; background-color: #f6f6f6; margin: 0; padding: 0;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;" width="100%" bgcolor="#f6f6f6">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto !important; max-width: 580px; padding: 10px; width: 580px;" width="580" valign="top">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Plex Requests Recently Added</span>
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #fff; border-radius: 3px;" width="100%">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr>
<td align="center">
<img src="http://i.imgur.com/s4nswSA.png?" width="400px" text-align="center" />
</td>
</tr>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">
<br/>
<br/>
<p style="font-family: sans-serif; font-size: 20px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Here is a list of Movies and TV Shows that have recently been added to Plex!</p>
</td>
</tr>
</table>
{@RECENTLYADDED}
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; padding-top: 10px; text-align: center; 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>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-top: 10px; padding-bottom: 10px; font-size: 12px; color: #999999; text-align: center;" valign="top" align="center">
Powered by <a href="https://github.com/tidusjar/PlexRequests.Net" style="color: #999999; font-size: 12px; text-align: center; text-decoration: underline;">Plex Requests .Net</a>
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
</tr>
</table>
</body>
</html>

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

@ -25,18 +25,13 @@
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System; using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using MailKit.Security;
using MimeKit; using MimeKit;
using NLog; using NLog;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.Models; using PlexRequests.Core.Models;
using PlexRequests.Core.Notification.Templates;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using SmtpClient = MailKit.Net.Smtp.SmtpClient; using SmtpClient = MailKit.Net.Smtp.SmtpClient;
@ -109,7 +104,7 @@ namespace PlexRequests.Services.Notification
if (string.IsNullOrEmpty(settings.EmailUsername) || string.IsNullOrEmpty(settings.EmailPassword)) if (string.IsNullOrEmpty(settings.EmailUsername) || string.IsNullOrEmpty(settings.EmailPassword))
{ {
return false; return false;
} }
} }
if (string.IsNullOrEmpty(settings.EmailHost) || string.IsNullOrEmpty(settings.RecipientEmail) || string.IsNullOrEmpty(settings.EmailPort.ToString())) if (string.IsNullOrEmpty(settings.EmailHost) || string.IsNullOrEmpty(settings.RecipientEmail) || string.IsNullOrEmpty(settings.EmailPort.ToString()))
{ {
@ -129,13 +124,16 @@ namespace PlexRequests.Services.Notification
private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings) private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings)
{ {
//var r = new NotificationMessageCurlys(model.User, model.Title, DateTime.Now.ToString(), model.RequestType.ToString(), string.Empty); var email = new EmailBasicTemplate();
//var resolver = new NotificationMessageResolver(); var html = email.LoadTemplate(
//var bodyResult = resolver.ParseMessage(settings, NotificationType.NewRequest, r); $"Plex Requests: New {model.RequestType.GetString()?.ToLower()} request for {model.Title}!",
$"Hello! The user '{model.User}' has requested the {model.RequestType.GetString()?.ToLower()} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." };
var message = new MimeMessage var message = new MimeMessage
{ {
Body = new TextPart("plain") { Text = $"Hello! The user '{model.User}' has requested the {model.RequestType.GetString()?.ToLower()} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}" }, Body = body.ToMessageBody(),
Subject = $"Plex Requests: New {model.RequestType.GetString()?.ToLower()} request for {model.Title}!" Subject = $"Plex Requests: New {model.RequestType.GetString()?.ToLower()} request for {model.Title}!"
}; };
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
@ -147,9 +145,16 @@ namespace PlexRequests.Services.Notification
private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings) private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings)
{ {
var email = new EmailBasicTemplate();
var html = email.LoadTemplate(
$"Plex Requests: New issue for {model.Title}!",
$"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." };
var message = new MimeMessage var message = new MimeMessage
{ {
Body = new TextPart("plain") { Text = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!" }, Body = body.ToMessageBody(),
Subject = $"Plex Requests: New issue for {model.Title}!" Subject = $"Plex Requests: New issue for {model.Title}!"
}; };
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
@ -165,10 +170,16 @@ namespace PlexRequests.Services.Notification
{ {
await Task.FromResult(false); await Task.FromResult(false);
} }
var email = new EmailBasicTemplate();
var html = email.LoadTemplate(
$"Plex Requests: {model.Title} is now available!",
$"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, TextBody = "This email is only available on devices that support HTML." };
var message = new MimeMessage var message = new MimeMessage
{ {
Body = new TextPart("plain") { Text = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)" }, Body = body.ToMessageBody(),
Subject = $"Plex Requests: {model.Title} is now available!" Subject = $"Plex Requests: {model.Title} is now available!"
}; };
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
@ -206,10 +217,15 @@ namespace PlexRequests.Services.Notification
private async Task EmailTest(NotificationModel model, EmailNotificationSettings settings) private async Task EmailTest(NotificationModel model, EmailNotificationSettings settings)
{ {
var email = new EmailBasicTemplate();
var html = email.LoadTemplate(
"Test Message",
"This is just a test! Success!",
model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, };
var message = new MimeMessage var message = new MimeMessage
{ {
Body = new TextPart("plain") { Text = "This is just a test! Success!" }, Body = body.ToMessageBody()
Subject = "Plex Requests: Test Message!",
}; };
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender)); message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail)); message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail));

@ -75,7 +75,7 @@ namespace PlexRequests.Services.Notification
if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase)) if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase))
{ {
Log.Info("This user is the Plex server owner"); Log.Info("This user is the Plex server owner");
await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title); await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath);
return; return;
} }
@ -88,7 +88,7 @@ namespace PlexRequests.Services.Notification
} }
Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title); Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title);
await PublishUserNotification(email.Username, email.Email, model.Title); await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath);
} }
} }
} }
@ -117,7 +117,7 @@ namespace PlexRequests.Services.Notification
if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase)) if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase))
{ {
Log.Info("This user is the Plex server owner"); Log.Info("This user is the Plex server owner");
await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title); await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath);
return; return;
} }
@ -130,7 +130,7 @@ namespace PlexRequests.Services.Notification
} }
Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title); Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title);
await PublishUserNotification(email.Username, email.Email, model.Title); await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath);
} }
} }
catch (Exception e) catch (Exception e)
@ -139,14 +139,15 @@ namespace PlexRequests.Services.Notification
} }
} }
private async Task PublishUserNotification(string username, string email, string title) private async Task PublishUserNotification(string username, string email, string title, string img)
{ {
var notificationModel = new NotificationModel var notificationModel = new NotificationModel
{ {
User = username, User = username,
UserEmail = email, UserEmail = email,
NotificationType = NotificationType.RequestAvailable, NotificationType = NotificationType.RequestAvailable,
Title = title Title = title,
ImgSrc = img
}; };
// Send the notification to the user. // Send the notification to the user.

@ -40,5 +40,6 @@ namespace PlexRequests.Services.Notification
public string User { get; set; } public string User { get; set; }
public string UserEmail { get; set; } public string UserEmail { get; set; }
public RequestType RequestType { get; set; } public RequestType RequestType { get; set; }
public string ImgSrc { get; set; }
} }
} }

@ -72,19 +72,26 @@
<Reference Include="Quartz, Version=2.3.3.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4"> <Reference Include="Quartz, Version=2.3.3.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4">
<HintPath>..\packages\Quartz.2.3.3\lib\net40\Quartz.dll</HintPath> <HintPath>..\packages\Quartz.2.3.3\lib\net40\Quartz.dll</HintPath>
</Reference> </Reference>
<Reference Include="TMDbLib, Version=0.9.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<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\IRecentlyAdded.cs" />
<Compile Include="Jobs\JobRecord.cs" /> <Compile Include="Jobs\JobRecord.cs" />
<Compile Include="Jobs\JobNames.cs" /> <Compile Include="Jobs\JobNames.cs" />
<Compile Include="Jobs\PlexEpisodeCacher.cs" /> <Compile Include="Jobs\PlexEpisodeCacher.cs" />
<Compile Include="Jobs\RecentlyAdded.cs" />
<Compile Include="Jobs\StoreBackup.cs" /> <Compile Include="Jobs\StoreBackup.cs" />
<Compile Include="Jobs\StoreCleanup.cs" /> <Compile Include="Jobs\StoreCleanup.cs" />
<Compile Include="Jobs\CouchPotatoCacher.cs" /> <Compile Include="Jobs\CouchPotatoCacher.cs" />
<Compile Include="Jobs\PlexAvailabilityChecker.cs" /> <Compile Include="Jobs\PlexAvailabilityChecker.cs" />
<Compile Include="Jobs\SickRageCacher.cs" /> <Compile Include="Jobs\SickRageCacher.cs" />
<Compile Include="Jobs\SonarrCacher.cs" /> <Compile Include="Jobs\SonarrCacher.cs" />
<Compile Include="Jobs\Templates\RecentlyAddedTemplate.cs" />
<Compile Include="Jobs\UserRequestLimitResetter.cs" /> <Compile Include="Jobs\UserRequestLimitResetter.cs" />
<Compile Include="Models\PlexAlbum.cs" /> <Compile Include="Models\PlexAlbum.cs" />
<Compile Include="Models\PlexEpisodeModel.cs" /> <Compile Include="Models\PlexEpisodeModel.cs" />
@ -97,6 +104,7 @@
<Compile Include="Interfaces\IIntervals.cs" /> <Compile Include="Interfaces\IIntervals.cs" />
<Compile Include="Interfaces\INotification.cs" /> <Compile Include="Interfaces\INotification.cs" />
<Compile Include="Interfaces\INotificationService.cs" /> <Compile Include="Interfaces\INotificationService.cs" />
<Compile Include="Models\SonarrCachedResult.cs" />
<Compile Include="Notification\EmailMessageNotification.cs" /> <Compile Include="Notification\EmailMessageNotification.cs" />
<Compile Include="Notification\NotificationEngine.cs" /> <Compile Include="Notification\NotificationEngine.cs" />
<Compile Include="Notification\NotificationModel.cs" /> <Compile Include="Notification\NotificationModel.cs" />
@ -136,6 +144,11 @@
<Name>PlexRequests.Store</Name> <Name>PlexRequests.Store</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="Jobs\Templates\RecentlyAddedTemplate.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</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.

@ -48,6 +48,7 @@ using PlexRequests.UI.Models;
using PlexRequests.UI.Modules; using PlexRequests.UI.Modules;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Analytics;
using PlexRequests.Services.Jobs;
using PlexRequests.UI.Helpers; using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Tests namespace PlexRequests.UI.Tests
@ -80,6 +81,8 @@ namespace PlexRequests.UI.Tests
private Mock<ISettingsService<LandingPageSettings>> LandingPageSettings { get; set; } private Mock<ISettingsService<LandingPageSettings>> LandingPageSettings { get; set; }
private Mock<ISlackApi> SlackApi { get; set; } private Mock<ISlackApi> SlackApi { get; set; }
private Mock<IAnalytics> Analytics { get; set; } private Mock<IAnalytics> Analytics { get; set; }
private Mock<ISettingsService<NotificationSettingsV2>> NotifyV2 { get; set; }
private Mock<IRecentlyAdded> RecentlyAdded { get; set; }
private ConfigurableBootstrapper Bootstrapper { get; set; } private ConfigurableBootstrapper Bootstrapper { get; set; }
@ -120,6 +123,8 @@ namespace PlexRequests.UI.Tests
ScheduledJobsSettingsMock = new Mock<ISettingsService<ScheduledJobsSettings>>(); ScheduledJobsSettingsMock = new Mock<ISettingsService<ScheduledJobsSettings>>();
RecorderMock = new Mock<IJobRecord>(); RecorderMock = new Mock<IJobRecord>();
Analytics = new Mock<IAnalytics>(); Analytics = new Mock<IAnalytics>();
NotifyV2= new Mock<ISettingsService<NotificationSettingsV2>>();
RecentlyAdded = new Mock<IRecentlyAdded>();
Bootstrapper = new ConfigurableBootstrapper(with => Bootstrapper = new ConfigurableBootstrapper(with =>
@ -140,6 +145,7 @@ namespace PlexRequests.UI.Tests
with.Dependency(LogRepo.Object); with.Dependency(LogRepo.Object);
with.Dependency(PushoverSettings.Object); with.Dependency(PushoverSettings.Object);
with.Dependency(PushoverApi.Object); with.Dependency(PushoverApi.Object);
with.Dependency(NotifyV2.Object);
with.Dependency(NotificationService.Object); with.Dependency(NotificationService.Object);
with.Dependency(Analytics.Object); with.Dependency(Analytics.Object);
with.Dependency(HeadphonesSettings.Object); with.Dependency(HeadphonesSettings.Object);
@ -150,6 +156,7 @@ namespace PlexRequests.UI.Tests
with.Dependency(SlackSettings.Object); with.Dependency(SlackSettings.Object);
with.Dependency(ScheduledJobsSettingsMock.Object); with.Dependency(ScheduledJobsSettingsMock.Object);
with.Dependency(RecorderMock.Object); with.Dependency(RecorderMock.Object);
with.Dependency(RecentlyAdded.Object);
with.RootPathProvider<TestRootPathProvider>(); with.RootPathProvider<TestRootPathProvider>();
with.RequestStartup((container, pipelines, context) => with.RequestStartup((container, pipelines, context) =>
{ {

@ -63,30 +63,18 @@ namespace PlexRequests.UI.Tests
} }
[Test] [Test]
public async Task HappyPathSendSeriesToSonarr() public async Task HappyPathSendSeriesToSonarrAllSeason()
{ {
var seriesResult = new SonarrAddSeries() { monitored = true }; var seriesResult = new SonarrAddSeries() { title = "ABC"};
SonarrMock.Setup(x => x.GetSeries(It.IsAny<string>(), It.IsAny<Uri>())).Returns(new List<Series>()); SonarrMock.Setup(x => x.GetSeries(It.IsAny<string>(), It.IsAny<Uri>())).Returns(F.Build<Series>().With(x => x.tvdbId, 1).With(x => x.title, "ABC").CreateMany().ToList());
SonarrMock.Setup(
x =>
x.AddSeries(
It.IsAny<int>(),
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<int[]>(),
It.IsAny<string>(),
It.IsAny<Uri>(),
It.IsAny<bool>(), It.IsAny<bool>())).Returns(seriesResult);
Sender = new TvSender(SonarrMock.Object, SickrageMock.Object); Sender = new TvSender(SonarrMock.Object, SickrageMock.Object);
var request = new RequestedModel(); var request = new RequestedModel {SeasonsRequested = "All", ProviderId = 1, Title = "ABC"};
var result = await Sender.SendToSonarr(GetSonarrSettings(), request); var result = await Sender.SendToSonarr(GetSonarrSettings(), request);
Assert.That(result, Is.EqualTo(seriesResult)); Assert.That(result.title, Is.EqualTo("ABC"));
SonarrMock.Verify(x => x.AddSeries(It.IsAny<int>(), SonarrMock.Verify(x => x.AddSeries(It.IsAny<int>(),
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int>(),
@ -96,7 +84,7 @@ namespace PlexRequests.UI.Tests
It.IsAny<int[]>(), It.IsAny<int[]>(),
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<Uri>(), It.IsAny<Uri>(),
true, It.IsAny<bool>()), Times.Once); true, It.IsAny<bool>()), Times.Never);
} }
[Test] [Test]

@ -468,8 +468,11 @@ $(function () {
episodes: result.episodes, episodes: result.episodes,
tvFullyAvailable: result.tvFullyAvailable, tvFullyAvailable: result.tvFullyAvailable,
url: result.plexUrl, url: result.plexUrl,
tvPartialAvailable : result.tvPartialAvailable tvPartialAvailable: result.tvPartialAvailable,
disableTvRequestsByEpisode: result.disableTvRequestsByEpisode,
disableTvRequestsBySeason: result.disableTvRequestsBySeason
}; };
return context; return context;
} }

@ -74,6 +74,11 @@ namespace PlexRequests.UI.Helpers
var series = await GetSonarrSeries(sonarrSettings, model.ProviderId); var series = await GetSonarrSeries(sonarrSettings, model.ProviderId);
var requestAll = model.SeasonsRequested?.Equals("All", StringComparison.CurrentCultureIgnoreCase);
var first = model.SeasonsRequested?.Equals("First", StringComparison.CurrentCultureIgnoreCase);
var latest = model.SeasonsRequested?.Equals("Latest", StringComparison.CurrentCultureIgnoreCase);
var specificSeasonRequest = model.SeasonList?.Any();
if (episodeRequest) if (episodeRequest)
{ {
// Does series exist? // Does series exist?
@ -112,58 +117,102 @@ namespace PlexRequests.UI.Helpers
return addResult; return addResult;
} }
if (series != null) // Series exists, don't need to add it
if (series == null)
{ {
var requestAll = model.SeasonsRequested.Equals("All", StringComparison.CurrentCultureIgnoreCase); // Set the series as monitored with a season count as 0 so it doesn't search for anything
var first = model.SeasonsRequested.Equals("First", StringComparison.CurrentCultureIgnoreCase); SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
var latest = model.SeasonsRequested.Equals("Latest", StringComparison.CurrentCultureIgnoreCase); sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, model.SeasonList, sonarrSettings.ApiKey,
sonarrSettings.FullUri);
if (model.SeasonList.Any()) await Task.Delay(TimeSpan.FromSeconds(1));
series = await GetSonarrSeries(sonarrSettings, model.ProviderId);
}
if (requestAll ?? false)
{
// Monitor all seasons
foreach (var season in series.seasons)
{ {
// Monitor the seasons that we have chosen season.monitored = true;
foreach (var season in series.seasons)
{
if (model.SeasonList.Contains(season.seasonNumber))
{
season.monitored = true;
}
}
} }
if (requestAll) SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
SonarrApi.SearchForSeries(series.id, sonarrSettings.ApiKey, sonarrSettings.FullUri); // Search For all episodes!"
return new SonarrAddSeries { title = series.title }; // We have updated it
}
if (first ?? false)
{
var firstSeries = (series?.seasons?.OrderBy(x => x.seasonNumber)).FirstOrDefault(x => x.seasonNumber > 0) ?? new Season();
firstSeries.monitored = true;
var episodes = SonarrApi.GetEpisodes(series.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri); // Need to get the episodes so we mark them as monitored
var episodesToUpdate = new List<SonarrEpisodes>();
foreach (var e in episodes)
{ {
// Monitor all seasons if (e.hasFile || e.seasonNumber != firstSeries.seasonNumber)
foreach (var season in series.seasons)
{ {
season.monitored = true; continue;
} }
e.monitored = true; // Mark only the episodes we want as monitored
episodesToUpdate.Add(e);
} }
foreach (var sonarrEpisode in episodesToUpdate)
if (first)
{ {
var firstSeries = series?.seasons?.OrderBy(x => x.seasonNumber)?.FirstOrDefault() ?? new Season(); SonarrApi.UpdateEpisode(sonarrEpisode, sonarrSettings.ApiKey, sonarrSettings.FullUri);
firstSeries.monitored = true;
} }
if (latest) SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
{ SonarrApi.SearchForSeason(series.id, firstSeries.seasonNumber, sonarrSettings.ApiKey,
var lastSeries = series?.seasons?.OrderByDescending(x => x.seasonNumber)?.FirstOrDefault() ?? new Season(); sonarrSettings.FullUri);
lastSeries.monitored = true; return new SonarrAddSeries { title = series.title }; // We have updated it
} }
if (latest ?? false)
{
var lastSeries = series?.seasons?.OrderByDescending(x => x.seasonNumber)?.FirstOrDefault() ?? new Season();
lastSeries.monitored = true;
// Update the series in sonarr with the new monitored status var episodes = SonarrApi.GetEpisodes(series.id.ToString(), sonarrSettings.ApiKey, sonarrSettings.FullUri); // Need to get the episodes so we mark them as monitored
var episodesToUpdate = new List<SonarrEpisodes>();
foreach (var e in episodes)
{
if (e.hasFile || e.seasonNumber != lastSeries.seasonNumber)
{
continue;
}
e.monitored = true; // Mark only the episodes we want as monitored
episodesToUpdate.Add(e);
}
foreach (var sonarrEpisode in episodesToUpdate)
{
SonarrApi.UpdateEpisode(sonarrEpisode, sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri); SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
await RequestAllEpisodesInASeasonWithExistingSeries(model, series, sonarrSettings); SonarrApi.SearchForSeason(series.id, lastSeries.seasonNumber, sonarrSettings.ApiKey,
sonarrSettings.FullUri);
return new SonarrAddSeries { title = series.title }; // We have updated it return new SonarrAddSeries { title = series.title }; // We have updated it
} }
if (specificSeasonRequest ?? false)
{
// Monitor the seasons that we have chosen
foreach (var season in series.seasons)
{
if (model.SeasonList.Contains(season.seasonNumber))
{
season.monitored = true;
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
SonarrApi.SearchForSeason(series.id, season.seasonNumber, sonarrSettings.ApiKey, sonarrSettings.FullUri);
}
}
return new SonarrAddSeries { title = series.title }; // We have updated it
}
var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, return null;
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey,
sonarrSettings.FullUri, true, true);
return result;
} }
public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model)
@ -231,7 +280,7 @@ namespace PlexRequests.UI.Helpers
var tasks = new List<Task>(); var tasks = new List<Task>();
foreach (var r in episodes) 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 if (r.monitored || r.hasFile) // If it's already monitored or has the file, there is no point in updating it
{ {
continue; continue;
} }
@ -262,7 +311,7 @@ namespace PlexRequests.UI.Helpers
var tasks = new List<Task>(); var tasks = new List<Task>();
var requestedEpisodes = model.Episodes; var requestedEpisodes = model.Episodes;
foreach (var r in episodes) foreach (var r in episodes)
{ {
if (r.hasFile) // If it already has the file, there is no point in updating it if (r.hasFile) // If it already has the file, there is no point in updating it

@ -65,7 +65,8 @@ namespace PlexRequests.UI.Jobs
JobBuilder.Create<CouchPotatoCacher>().WithIdentity("CouchPotatoCacher", "Cache").Build(), JobBuilder.Create<CouchPotatoCacher>().WithIdentity("CouchPotatoCacher", "Cache").Build(),
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()
}; };
@ -165,6 +166,13 @@ namespace PlexRequests.UI.Jobs
.WithSimpleSchedule(x => x.WithIntervalInHours(s.PlexEpisodeCacher).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInHours(s.PlexEpisodeCacher).RepeatForever())
.Build(); .Build();
var rencentlyAdded =
TriggerBuilder.Create()
.WithIdentity("RecentlyAdded", "Email")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInHours(2).RepeatForever())
.Build();
triggers.Add(plexAvailabilityChecker); triggers.Add(plexAvailabilityChecker);
triggers.Add(srCacher); triggers.Add(srCacher);
@ -174,6 +182,7 @@ 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;
} }

@ -55,5 +55,7 @@ namespace PlexRequests.UI.Models
public int SiteRating { get; set; } public int SiteRating { get; set; }
public List<Store.EpisodesModel> Episodes { get; set; } public List<Store.EpisodesModel> Episodes { get; set; }
public bool TvFullyAvailable { get; set; } public bool TvFullyAvailable { get; set; }
public bool DisableTvRequestsByEpisode { get; set; }
public bool DisableTvRequestsBySeason { get; set; }
} }
} }

@ -57,6 +57,7 @@ using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Exceptions; using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs;
using PlexRequests.Services.Notification; using PlexRequests.Services.Notification;
using PlexRequests.Store.Models; using PlexRequests.Store.Models;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
@ -94,6 +95,8 @@ namespace PlexRequests.UI.Modules
private ISlackApi SlackApi { get; } private ISlackApi SlackApi { get; }
private IJobRecord JobRecorder { get; } private IJobRecord JobRecorder { get; }
private IAnalytics Analytics { get; } private IAnalytics Analytics { get; }
private IRecentlyAdded RecentlyAdded { get; }
private ISettingsService<NotificationSettingsV2> NotifySettings { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
public AdminModule(ISettingsService<PlexRequestSettings> prService, public AdminModule(ISettingsService<PlexRequestSettings> prService,
@ -116,7 +119,8 @@ namespace PlexRequests.UI.Modules
ISettingsService<LogSettings> logs, ISettingsService<LogSettings> logs,
ICacheProvider cache, ISettingsService<SlackNotificationSettings> slackSettings, ICacheProvider cache, ISettingsService<SlackNotificationSettings> slackSettings,
ISlackApi slackApi, ISettingsService<LandingPageSettings> lp, ISlackApi slackApi, ISettingsService<LandingPageSettings> lp,
ISettingsService<ScheduledJobsSettings> scheduler, IJobRecord rec, IAnalytics analytics) : base("admin", prService) ISettingsService<ScheduledJobsSettings> scheduler, IJobRecord rec, IAnalytics analytics,
ISettingsService<NotificationSettingsV2> notifyService, IRecentlyAdded recentlyAdded) : base("admin", prService)
{ {
PrService = prService; PrService = prService;
CpService = cpService; CpService = cpService;
@ -143,6 +147,8 @@ namespace PlexRequests.UI.Modules
ScheduledJobSettings = scheduler; ScheduledJobSettings = scheduler;
JobRecorder = rec; JobRecorder = rec;
Analytics = analytics; Analytics = analytics;
NotifySettings = notifyService;
RecentlyAdded = recentlyAdded;
this.RequiresClaims(UserClaims.Admin); this.RequiresClaims(UserClaims.Admin);
@ -210,6 +216,11 @@ namespace PlexRequests.UI.Modules
Post["/scheduledjobs", true] = async (x, ct) => await SaveScheduledJobs(); Post["/scheduledjobs", true] = async (x, ct) => await SaveScheduledJobs();
Post["/clearlogs", true] = async (x, ct) => await ClearLogs(); Post["/clearlogs", true] = async (x, ct) => await ClearLogs();
Get["/notificationsettings", true] = async (x, ct) => await NotificationSettings();
Post["/notificationsettings", true] = async (x, ct) => await SaveNotificationSettings();
Post["/recentlyAddedTest"] = x => RecentlyAddedTest();
} }
private async Task<Negotiator> Authentication() private async Task<Negotiator> Authentication()
@ -489,7 +500,8 @@ namespace PlexRequests.UI.Modules
var notificationModel = new NotificationModel var notificationModel = new NotificationModel
{ {
NotificationType = NotificationType.Test, NotificationType = NotificationType.Test,
DateTime = DateTime.Now DateTime = DateTime.Now,
ImgSrc = "http://3.bp.blogspot.com/-EFM-XoKoZ0o/UznF567wCRI/AAAAAAAAALM/6ut7MCF2LrU/s1600/xkcd.png"
}; };
try try
{ {
@ -966,5 +978,31 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message }); return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message });
} }
} }
private async Task<Negotiator> NotificationSettings()
{
var s = await NotifySettings.GetSettingsAsync();
return View["NotificationSettings", s];
}
private async Task<Negotiator> SaveNotificationSettings()
{
var model = this.Bind<NotificationSettingsV2>();
return View["NotificationSettings", model];
}
private Response RecentlyAddedTest()
{
try
{
RecentlyAdded.Test();
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Sent email to administrator" });
}
catch (Exception e)
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message });
}
}
} }
} }

@ -290,8 +290,10 @@ namespace PlexRequests.UI.Modules
private async Task<Response> SearchTvShow(string searchTerm) private async Task<Response> SearchTvShow(string searchTerm)
{ {
Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies)); Analytics.TrackEventAsync(Category.Search, Action.TvShow, searchTerm, Username, CookieHelper.GetAnalyticClientId(Cookies));
var plexSettings = await PlexService.GetSettingsAsync(); var plexSettings = await PlexService.GetSettingsAsync();
var prSettings = await PrService.GetSettingsAsync();
var providerId = string.Empty; var providerId = string.Empty;
var apiTv = new List<TvMazeSearch>(); var apiTv = new List<TvMazeSearch>();
@ -336,7 +338,9 @@ namespace PlexRequests.UI.Modules
Runtime = t.show.runtime.ToString(), Runtime = t.show.runtime.ToString(),
SeriesId = t.show.id, SeriesId = t.show.id,
SeriesName = t.show.name, SeriesName = t.show.name,
Status = t.show.status Status = t.show.status,
DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode,
DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason
}; };
@ -362,9 +366,8 @@ namespace PlexRequests.UI.Modules
viewT.Requested = true; viewT.Requested = true;
viewT.Episodes = dbt.Episodes.ToList(); viewT.Episodes = dbt.Episodes.ToList();
viewT.Approved = dbt.Approved; viewT.Approved = dbt.Approved;
viewT.Available = dbt.Available;
} }
if (sonarrCached.Contains(tvdbid) || sickRageCache.Contains(tvdbid)) // compare to the sonarr/sickrage db if (sonarrCached.Select(x => x.TvdbId).Contains(tvdbid) || sickRageCache.Contains(tvdbid)) // compare to the sonarr/sickrage db
{ {
viewT.Requested = true; viewT.Requested = true;
} }
@ -570,7 +573,7 @@ namespace PlexRequests.UI.Modules
if (showInfo.externals?.thetvdb == null) if (showInfo.externals?.thetvdb == null)
{ {
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Our TV Provider (TVMaze) doesn't have a TheTVDBId for this TV Show :( We cannot add the TV Show automatically sorry! Please report this problem to the server admin so he can sort it out!" }); return Response.AsJson(new JsonResponseModel { Result = false, Message = "Our TV Provider (TVMaze) doesn't have a TheTVDBId for this TV Show :( We cannot add the TV Show automatically sorry! Please report this problem to the server admin so he/she can sort it out!" });
} }
var model = new RequestedModel var model = new RequestedModel

@ -45,6 +45,7 @@ namespace PlexRequests.UI.NinjectModules
Bind<ICouchPotatoCacher>().To<CouchPotatoCacher>(); Bind<ICouchPotatoCacher>().To<CouchPotatoCacher>();
Bind<ISonarrCacher>().To<SonarrCacher>(); Bind<ISonarrCacher>().To<SonarrCacher>();
Bind<ISickRageCacher>().To<SickRageCacher>(); Bind<ISickRageCacher>().To<SickRageCacher>();
Bind<IRecentlyAdded>().To<RecentlyAdded>();
Bind<IJobFactory>().To<CustomJobFactory>(); Bind<IJobFactory>().To<CustomJobFactory>();
Bind<IAnalytics>().To<Analytics>(); Bind<IAnalytics>().To<Analytics>();

@ -707,6 +707,9 @@
<Content Include="Views\UserWizard\Index.cshtml"> <Content Include="Views\UserWizard\Index.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Views\Admin\NotificationSettings.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="Web.Debug.config"> <None Include="Web.Debug.config">
<DependentUpon>web.config</DependentUpon> <DependentUpon>web.config</DependentUpon>
</None> </None>

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -121,7 +121,7 @@
<value>Anmelden</value> <value>Anmelden</value>
</data> </data>
<data name="UserLogin_Paragraph" xml:space="preserve"> <data name="UserLogin_Paragraph" xml:space="preserve">
<value>Möchten Sie einen Film oder eine Serie sehen, welche/r nicht auf Plex ist? Loggen Sie sich unten mit Ihrem Plex.tv Benutzernamen und Passwort ein!</value> <value>Möchten Sie einen Film oder eine Serie schauen, die momentan noch nicht auf Plex ist? Dann loggen Sie sich unten ein und fordern Sie das Material an!</value>
</data> </data>
<data name="UserLogin_Paragraph_SpanHover" xml:space="preserve"> <data name="UserLogin_Paragraph_SpanHover" xml:space="preserve">
<value>Ihre Login-Daten werden nur zur Authorisierung Ihres Plex-Konto verwendet.</value> <value>Ihre Login-Daten werden nur zur Authorisierung Ihres Plex-Konto verwendet.</value>
@ -172,7 +172,7 @@
<value>Ausloggen</value> <value>Ausloggen</value>
</data> </data>
<data name="Layout_UpdateAvailablePart1" xml:space="preserve"> <data name="Layout_UpdateAvailablePart1" xml:space="preserve">
<value>Es ist ein neues Update verfügbar! Klicken</value> <value>Es ist ein neues Update verfügbar! Hier Klicken</value>
</data> </data>
<data name="Layout_English" xml:space="preserve"> <data name="Layout_English" xml:space="preserve">
<value>Englisch</value> <value>Englisch</value>
@ -211,7 +211,7 @@
<value>Alben</value> <value>Alben</value>
</data> </data>
<data name="Search_Paragraph" xml:space="preserve"> <data name="Search_Paragraph" xml:space="preserve">
<value>Möchten Sie etwas zu sehen, das derzeit nicht auf Plex ist ?! Kein Problem! Suchen Sie einfach unten danach und fragen Sie es an!</value> <value>Möchten Sie etwas schauen, das derzeit nicht auf Plex ist?! Kein Problem! Suchen Sie unten einfach danach und fragen Sie es an!</value>
</data> </data>
<data name="Search_Title" xml:space="preserve"> <data name="Search_Title" xml:space="preserve">
<value>Suche</value> <value>Suche</value>
@ -220,13 +220,13 @@
<value>Vorschläge</value> <value>Vorschläge</value>
</data> </data>
<data name="Search_ComingSoon" xml:space="preserve"> <data name="Search_ComingSoon" xml:space="preserve">
<value>Demnächst</value> <value>Demnächst verfügbar</value>
</data> </data>
<data name="Search_InTheaters" xml:space="preserve"> <data name="Search_InTheaters" xml:space="preserve">
<value>In den Kinos</value> <value>Momentan im Kino</value>
</data> </data>
<data name="Search_SendNotificationText" xml:space="preserve"> <data name="Search_SendNotificationText" xml:space="preserve">
<value>Senden Sie mir eine Benachrichtigung, wenn Gegenstände, die ich angefordert habe hinzugefügt wurden.</value> <value>Sende mir eine Benachrichtigung, wenn die Serien oder die Filme, die ich angefordert habe hinzugefügt wurden.</value>
</data> </data>
<data name="Common_Save" xml:space="preserve"> <data name="Common_Save" xml:space="preserve">
<value>Speichern</value> <value>Speichern</value>
@ -235,13 +235,13 @@
<value>Verfügbar</value> <value>Verfügbar</value>
</data> </data>
<data name="Search_Requested" xml:space="preserve"> <data name="Search_Requested" xml:space="preserve">
<value>angefragt</value> <value>angefordert</value>
</data> </data>
<data name="Search_Request" xml:space="preserve"> <data name="Search_Request" xml:space="preserve">
<value>Angefordert</value> <value>Anfordern</value>
</data> </data>
<data name="Search_AllSeasons" xml:space="preserve"> <data name="Search_AllSeasons" xml:space="preserve">
<value>alle Staffeln</value> <value>Alle Staffeln</value>
</data> </data>
<data name="Search_FirstSeason" xml:space="preserve"> <data name="Search_FirstSeason" xml:space="preserve">
<value>Erste Staffel</value> <value>Erste Staffel</value>
@ -253,10 +253,10 @@
<value>Auswählen</value> <value>Auswählen</value>
</data> </data>
<data name="Search_ReportIssue" xml:space="preserve"> <data name="Search_ReportIssue" xml:space="preserve">
<value>Melde Problem</value> <value>Problem melden</value>
</data> </data>
<data name="Issues_WrongAudio" xml:space="preserve"> <data name="Issues_WrongAudio" xml:space="preserve">
<value>Falsche Audio</value> <value>Falscher Ton</value>
</data> </data>
<data name="Issues_NoSubs" xml:space="preserve"> <data name="Issues_NoSubs" xml:space="preserve">
<value>Keine Untertitel</value> <value>Keine Untertitel</value>
@ -268,7 +268,7 @@
<value>Wiedergabe-Probleme</value> <value>Wiedergabe-Probleme</value>
</data> </data>
<data name="Issues_Other" xml:space="preserve"> <data name="Issues_Other" xml:space="preserve">
<value>Sonstige</value> <value>Sonstiges</value>
</data> </data>
<data name="Search_TrackCount" xml:space="preserve"> <data name="Search_TrackCount" xml:space="preserve">
<value>Track-Count</value> <value>Track-Count</value>
@ -280,7 +280,7 @@
<value>Staffeln</value> <value>Staffeln</value>
</data> </data>
<data name="Common_Close" xml:space="preserve"> <data name="Common_Close" xml:space="preserve">
<value>Schließen</value> <value>Schliessen</value>
</data> </data>
<data name="Issues_Modal_Title" xml:space="preserve"> <data name="Issues_Modal_Title" xml:space="preserve">
<value>Fügen Sie ein Problem hinzu</value> <value>Fügen Sie ein Problem hinzu</value>
@ -298,7 +298,7 @@
<value>Anfragen</value> <value>Anfragen</value>
</data> </data>
<data name="Requests_Paragraph" xml:space="preserve"> <data name="Requests_Paragraph" xml:space="preserve">
<value>Im Folgenden finden Sie Ihre und alle anderen Anfragen, sowie deren Download- und Genehmigungsstatus angezeigt.</value> <value>Unten befinden sich alle Anfragen aller Benutzer. Hier ist auch der aktuelle Status des beantragten Mediums ersichtlich.</value>
</data> </data>
<data name="Requests_MoviesTabTitle" xml:space="preserve"> <data name="Requests_MoviesTabTitle" xml:space="preserve">
<value>Filme</value> <value>Filme</value>
@ -349,7 +349,7 @@
<value>Nicht veröffentlicht</value> <value>Nicht veröffentlicht</value>
</data> </data>
<data name="Requests_Order" xml:space="preserve"> <data name="Requests_Order" xml:space="preserve">
<value>Sortieren nach</value> <value>Sortieren nach</value>
</data> </data>
<data name="Requests_Filter" xml:space="preserve"> <data name="Requests_Filter" xml:space="preserve">
<value>Filter</value> <value>Filter</value>
@ -376,7 +376,7 @@
<value>Beantragt von</value> <value>Beantragt von</value>
</data> </data>
<data name="Requests_RequestedDate" xml:space="preserve"> <data name="Requests_RequestedDate" xml:space="preserve">
<value>angefragt</value> <value>Angefragt vor</value>
</data> </data>
<data name="Requests_ToggleDropdown" xml:space="preserve"> <data name="Requests_ToggleDropdown" xml:space="preserve">
<value>Toggle Dropdown</value> <value>Toggle Dropdown</value>
@ -388,10 +388,10 @@
<value>Entfernen</value> <value>Entfernen</value>
</data> </data>
<data name="Requests_MarkUnavailable" xml:space="preserve"> <data name="Requests_MarkUnavailable" xml:space="preserve">
<value>Markeiren als "Nicht verfügbar"</value> <value>Als "Nicht verfügbar" markieren</value>
</data> </data>
<data name="Requests_MarkAvailable" xml:space="preserve"> <data name="Requests_MarkAvailable" xml:space="preserve">
<value>Markieren als "Verfügbar"</value> <value>Als "Verfügbar" markieren</value>
</data> </data>
<data name="Common_Approved" xml:space="preserve"> <data name="Common_Approved" xml:space="preserve">
<value>Genehmigt</value> <value>Genehmigt</value>
@ -409,40 +409,40 @@
<value>wurde bereits angefragt!</value> <value>wurde bereits angefragt!</value>
</data> </data>
<data name="Search_CouldNotCheckPlex" xml:space="preserve"> <data name="Search_CouldNotCheckPlex" xml:space="preserve">
<value>Wir konnten nicht prüfen ob {0} in Plex ist. Bist du sicher dass es richtig installiert ist?</value> <value>Wir konnten nicht prüfen ob {0} bereits auf Plex ist. Bist du sicher dass alles richtig installiert ist?</value>
</data> </data>
<data name="Search_CouchPotatoError" xml:space="preserve"> <data name="Search_CouchPotatoError" xml:space="preserve">
<value>Etwas ging schief beim hinzufügen des Filmes zu CouchPotato! Bitte überprüfe deine Einstellungen.</value> <value>Etwas ging etwas schief beim hinzufügen des Filmes zu CouchPotato! Bitte überprüfe deine Einstellungen.</value>
</data> </data>
<data name="Search_WeeklyRequestLimitMovie" xml:space="preserve"> <data name="Search_WeeklyRequestLimitMovie" xml:space="preserve">
<value>Du hast deine wöchentliche Maximalanfragenanzahl für Filme erreicht. Bitte kontaktiere deinen Admin.</value> <value>Du hast deine wöchentliche Maximalanfragen für neue Filme erreicht. Bitte kontaktiere den Administrator.</value>
</data> </data>
<data name="Search_AlreadyInPlex" xml:space="preserve"> <data name="Search_AlreadyInPlex" xml:space="preserve">
<value>ist bereits in Plex!</value> <value>ist bereits auf Plex!</value>
</data> </data>
<data name="Search_SickrageError" xml:space="preserve"> <data name="Search_SickrageError" xml:space="preserve">
<value>Etwas ging schief beim hinzufügen des Filmes zu SickRage! Bitte überprüfe deine Einstellungen.</value> <value>Etwas ging etwas schief beim hinzufügen des Filmes zu SickRage! Bitte überprüfe deine Einstellungen.</value>
</data> </data>
<data name="Search_TvNotSetUp" xml:space="preserve"> <data name="Search_TvNotSetUp" xml:space="preserve">
<value>The Anfrage für Serien ist nicht richtig installiert. Bitte kontaktiere deinen Admin.</value> <value>Die Anfrage für Serien ist momentan nicht richtig installiert. Bitte kontaktiere den Administrator.</value>
</data> </data>
<data name="Search_WeeklyRequestLimitAlbums" xml:space="preserve"> <data name="Search_WeeklyRequestLimitAlbums" xml:space="preserve">
<value>Du hast deine wöchentliche Maximalanfragenanzahl für Alben erreicht. Bitte kontaktiere deinen Admin.</value> <value>Du hast deine wöchentliche Maximalanfragen für neue Alben erreicht. Bitte kontaktiere den Administrator.</value>
</data> </data>
<data name="Search_MusicBrainzError" xml:space="preserve"> <data name="Search_MusicBrainzError" xml:space="preserve">
<value>Wir konnten den Interpreten auf MusicBrainz nicht finden. Bitte versuche es später erneut oder kontaktiere deinen Admin.</value> <value>Wir konnten den Interpreten auf MusicBrainz leider nicht finden. Bitte versuche es später erneut oder kontaktiere den Administrator.</value>
</data> </data>
<data name="Search_WeeklyRequestLimitTVShow" xml:space="preserve"> <data name="Search_WeeklyRequestLimitTVShow" xml:space="preserve">
<value>Du hast deine wöchentliche Maximalanfragenanzahl für Serien erreicht. Bitte kontaktiere deinen Admin.</value> <value>Du hast deine wöchentliche Maximalanfragen für neue Serien erreicht. Bitte kontaktiere den Administrator.</value>
</data> </data>
<data name="Search_ErrorPlexAccountOnly" xml:space="preserve"> <data name="Search_ErrorPlexAccountOnly" xml:space="preserve">
<value>Entschuldige, aber diese Funktion ist momentan nur für Benutzer mit Plex-Accounts freigeschalten.</value> <value>Entschuldige, aber diese Funktion ist momentan nur für Benutzer mit Plex-Accounts freigeschaltet.</value>
</data> </data>
<data name="Search_ErrorNotEnabled" xml:space="preserve"> <data name="Search_ErrorNotEnabled" xml:space="preserve">
<value>Entschuldige, aber dein Admin hat diese Funktion noch nicht freigeschalten.</value> <value>Entschuldige, aber dein Administrator hat diese Funktion noch nicht freigeschaltet.</value>
</data> </data>
<data name="Search_NotificationError" xml:space="preserve"> <data name="Search_NotificationError" xml:space="preserve">
<value>Wir konnten diese Meldung nicht entfernen, weil du es nie hattest.</value> <value>Wir konnten diese Meldung nicht entfernen.</value>
</data> </data>
<data name="Common_CouldNotSave" xml:space="preserve"> <data name="Common_CouldNotSave" xml:space="preserve">
<value>Speichern fehlgeschlagen. Bitte versuche es erneut.</value> <value>Speichern fehlgeschlagen. Bitte versuche es erneut.</value>
@ -451,12 +451,12 @@
<value>Französisch</value> <value>Französisch</value>
</data> </data>
<data name="Search_SelectEpisode" xml:space="preserve"> <data name="Search_SelectEpisode" xml:space="preserve">
<value>Wählen Sie Episode</value> <value>Wählen Sie ihre Episode</value>
</data> </data>
<data name="UserLogin_IncorrectUserPass" xml:space="preserve"> <data name="UserLogin_IncorrectUserPass" xml:space="preserve">
<value>Falsche Benutzer oder Passwort</value> <value>Falscher Benutzer oder Passwort</value>
</data> </data>
<data name="Requests_ReleaseDate_Unavailable" xml:space="preserve"> <data name="Requests_ReleaseDate_Unavailable" xml:space="preserve">
<value>Es gibt keine Informationen für die Release-Termin</value> <value>Es gibt noch keine Informationen für diesen Release-Termin</value>
</data> </data>
</root> </root>

@ -160,6 +160,7 @@
$('#requestToken').click(function (e) { $('#requestToken').click(function (e) {
e.preventDefault(); e.preventDefault();
debugger;
var $form = $("#mainForm"); var $form = $("#mainForm");
$.ajax({ $.ajax({
type: $form.prop("method"), type: $form.prop("method"),
@ -167,7 +168,7 @@
data: $form.serialize(), data: $form.serialize(),
dataType: "json", dataType: "json",
success: function (response) { success: function (response) {
if (response.result === true) { if (response.apiKey) {
generateNotify("Success!", "success"); generateNotify("Success!", "success");
$('#ApiKey').val(response.apiKey); $('#ApiKey').val(response.apiKey);
} else { } else {

@ -0,0 +1,84 @@
@using System.Linq
@using PlexRequests.Core.Models
@using PlexRequests.UI.Helpers
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.NotificationSettingsV2>
@Html.Partial("_Sidebar")
<div class="col-sm-8 col-sm-push-1">
<form class="form-horizontal" method="POST" id="mainForm">
<fieldset>
<legend>Notification Settings</legend>
<!--Accordion Item-->
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="0headingOne">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#0collapseOne" aria-controls="0collapseOne">
New Request
</a>
</h4>
</div>
<div id="0collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="0headingOne">
<div class="panel-body">
<div class="form-group">
<label for="EmailNotification[0].Subject" class="control-label">Subject</label>
<div>
<input type="text" class="form-control form-control-custom " id="EmailNotification[0].Subject" name="EmailNotification0.Subject" value="@(Model.EmailNotification[0].Subject)">
</div>
</div>
<div class="form-group">
<label for="EmailNotification[0].Body" class="control-label">Body</label>
<div>
<input type="text" class="form-control form-control-custom " id="EmailNotification[0].Body" name="EmailNotification0.Body" value="@(Model.EmailNotification[0].Body)">
</div>
</div>
</div>
</div>
</div>
}
</div>
<div class="form-group">
<div>
<button id="save" type="submit" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</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");
}
});
});
});
</script>

@ -6,15 +6,19 @@
<div class="row"> <div class="row">
<div class="col-md-3"><strong>Job Name</strong></div> <div class="col-md-4"><strong>Job Name</strong>
<div class="col-md-8"><strong>Last Run</strong></div> </div>
<div class="col-md-6 col-md-push-3"><strong>Last Run</strong>
</div>
</div> </div>
<hr style="margin-top: 4px; margin-bottom: 4px"/>
@foreach (var record in Model.JobRecorder) @foreach (var record in Model.JobRecorder)
{ {
<div class="row"> <div class="row">
<div class="col-md-3">@record.Key</div> <div class="col-md-4">@record.Key</div>
<div class="col-md-8 date">@record.Value.ToString("O")</div> <div class="col-md-5 col-md-push-3 date">@record.Value.ToString("O")</div>
</div> </div>
<hr style="margin-top: 4px; margin-bottom: 4px"/>
} }
<br/> <br/>
<br/> <br/>
@ -35,7 +39,7 @@
<small>Please note, the minimum time for this to run is 11 hours, if set below 11 then we will ignore that value. This is a very resource intensive job, the less we run it the better.</small> <small>Please note, the minimum time for this to run is 11 hours, if set below 11 then we will ignore that value. This is a very resource intensive job, the less we run it the better.</small>
<div class="form-group"> <div class="form-group">
<label for="PlexEpisodeCacher" class="control-label">Plex Episode Cacher (hour)</label> <label for="PlexEpisodeCacher" class="control-label">Plex Episode Cacher (hours)</label>
<input type="text" class="form-control form-control-custom " id="PlexEpisodeCacher" name="PlexEpisodeCacher" value="@Model.PlexEpisodeCacher"> <input type="text" class="form-control form-control-custom " id="PlexEpisodeCacher" name="PlexEpisodeCacher" value="@Model.PlexEpisodeCacher">
</div> </div>
@ -54,27 +58,34 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="StoreBackup" class="control-label">Store Backup (hour)</label> <label for="StoreBackup" class="control-label">Store Backup (hours)</label>
<div> <div>
<input type="text" class="form-control form-control-custom " id="StoreBackup" name="StoreBackup" value="@Model.StoreBackup"> <input type="text" class="form-control form-control-custom " id="StoreBackup" name="StoreBackup" value="@Model.StoreBackup">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="StoreCleanup" class="control-label">Store Cleanup (hour)</label> <label for="StoreCleanup" class="control-label">Store Cleanup (hours)</label>
<div> <div>
<input type="text" class="form-control form-control-custom " id="StoreCleanup" name="StoreCleanup" value="@Model.StoreCleanup"> <input type="text" class="form-control form-control-custom " id="StoreCleanup" name="StoreCleanup" value="@Model.StoreCleanup">
</div> </div>
</div> </div>
<small>Please note, this will not reset the users request limit, it will just check every X hours to see if it needs to be reset.</small> <small>Please note, this will not reset the users request limit, it will just check every @Model.UserRequestLimitResetter hours to see if it needs to be reset.</small>
<div class="form-group"> <div class="form-group">
<label for="UserRequestLimitResetter" class="control-label">User Request Limit Reset (hour)</label> <label for="UserRequestLimitResetter" class="control-label">User Request Limit Reset (hours)</label>
<div> <div>
<input type="text" class="form-control form-control-custom " id="UserRequestLimitResetter" name="UserRequestLimitResetter" value="@Model.UserRequestLimitResetter"> <input type="text" class="form-control form-control-custom " id="UserRequestLimitResetter" name="UserRequestLimitResetter" value="@Model.UserRequestLimitResetter">
</div> </div>
</div> </div>
<div class="form-group">
<label for="RecentlyAdded" class="control-label">Recently Added Email (hours)</label>
<div>
<input type="text" class="form-control form-control-custom " id="RecentlyAdded" name="RecentlyAdded" value="@Model.RecentlyAdded">
</div>
</div>
<div class="form-group"> <div class="form-group">
<div> <div>
<button id="save" type="submit" class="btn btn-primary-outline ">Submit</button> <button id="save" type="submit" class="btn btn-primary-outline ">Submit</button>

@ -65,17 +65,37 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="select" class="control-label">Theme</label> <label for="select" class="control-label">Theme</label>
<div id="themes"> <div id="themes">
<select class="form-control form-control-custom" id="select"> <select class="form-control form-control-custom" id="select">
<option @plexTheme class="form-control form-control-custom" value="@Themes.PlexTheme">Plex</option> <option @plexTheme class="form-control form-control-custom" value="@Themes.PlexTheme">Plex</option>
<option @originalTheme class="form-control form-control-custom" value="@Themes.OriginalTheme">Original Blue</option> <option @originalTheme class="form-control form-control-custom" value="@Themes.OriginalTheme">Original Blue</option>
</select> </select>
</div>
</div> </div>
</div>
<br/>
<br/>
<div class="form-group">
<div class="checkbox">
<div class="form-group"> <small>Note: This will require you to setup your email notifications</small>
@if (Model.SendRecentlyAddedEmail)
{
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail" checked="checked"><label for="SendRecentlyAddedEmail">Send out a weekly email of recently added content to all your Plex 'Friends'</label>
}
else
{
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail"><label for="SendRecentlyAddedEmail">Send out a weekly email of recently added content to all your Plex 'Friends'</label>
}
</div>
</div>
<button id="recentlyAddedBtn" class="btn btn-primary-outline">Send test email to Admin</button>
<br/>
<br/>
<div class="form-group">
<div class="checkbox"> <div class="checkbox">
@if (Model.SearchForMovies) @if (Model.SearchForMovies)
@ -193,6 +213,36 @@
} }
</div> </div>
</div> </div>
<div class="form-group">
<div class="checkbox">
@if (Model.DisableTvRequestsByEpisode)
{
<input type="checkbox" id="DisableTvRequestsByEpisode" name="DisableTvRequestsByEpisode" checked="checked">
<label for="DisableTvRequestsByEpisode">Disable TV requests by episode</label>
}
else
{
<input type="checkbox" id="DisableTvRequestsByEpisode" name="DisableTvRequestsByEpisode"><label for="DisableTvRequestsByEpisode">Disable TV requests by episode</label>
}
</div>
</div>
<div class="form-group">
<div class="checkbox">
@if (Model.DisableTvRequestsBySeason)
{
<input type="checkbox" id="DisableTvRequestsBySeason" name="DisableTvRequestsBySeason" checked="checked">
<label for="DisableTvRequestsBySeason">Disable TV requests by season</label>
}
else
{
<input type="checkbox" id="DisableTvRequestsBySeason" name="DisableTvRequestsBySeason"><label for="DisableTvRequestsBySeason">Disable TV requests by season</label>
}
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
@ -325,5 +375,29 @@
} }
}); });
}); });
$('#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);
generateNotify("Something went wrong!", "danger");
}
});
});
}); });
</script> </script>

@ -168,12 +168,15 @@
{{/if_eq}} {{/if_eq}}
{{#if_eq type "tv"}} {{#if_eq type "tv"}}
{{#if available}} {{#if available}}
<span class="label label-success">Available</span> <span class="label label-success">Available on Plex</span>
{{else}} {{else}}
<span class="label label-danger">Not Available</span> {{#if approved}}
<span class="label label-info">Processing request</span>
{{else if requested}}
<span class="label label-warning">Pending approval</span>
{{else}}
<span class="label label-danger">Not Requested yet</span>
{{/if}} {{/if}}
{{#if requested}}
<span class="label label-success">Requested</span>
{{/if}} {{/if}}
<br /> <br />
<br /> <br />
@ -210,10 +213,14 @@
</button> </button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1"> <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a id="{{id}}" season-select="0" class="dropdownTv " href="#">@UI.Search_AllSeasons</a></li> <li><a id="{{id}}" season-select="0" class="dropdownTv " href="#">@UI.Search_AllSeasons</a></li>
{{#if_eq disableTvRequestsBySeason false}}
<li><a id="{{id}}" season-select="1" class="dropdownTv" href="#">@UI.Search_FirstSeason</a></li> <li><a id="{{id}}" season-select="1" class="dropdownTv" href="#">@UI.Search_FirstSeason</a></li>
<li><a id="{{id}}" season-select="2" class="dropdownTv" href="#">@UI.Search_LatestSeason</a></li> <li><a id="{{id}}" season-select="2" class="dropdownTv" href="#">@UI.Search_LatestSeason</a></li>
<li><a id="SeasonSelect" data-identifier="{{id}}" data-toggle="modal" data-target="#seasonsModal" href="#">@UI.Search_SelectSeason...</a></li> <li><a id="SeasonSelect" data-identifier="{{id}}" data-toggle="modal" data-target="#seasonsModal" href="#">@UI.Search_SelectSeason...</a></li>
{{/if_eq}}
{{#if_eq disableTvRequestsByEpisode false}}
<li><a id="EpisodeSelect" data-identifier="{{id}}" data-toggle="modal" data-target="#episodesModal" href="#">@UI.Search_SelectEpisode...</a></li> <li><a id="EpisodeSelect" data-identifier="{{id}}" data-toggle="modal" data-target="#episodesModal" href="#">@UI.Search_SelectEpisode...</a></li>
{{/if_eq}}
</ul> </ul>
</div> </div>
{{#if available}} {{#if available}}

@ -19,6 +19,7 @@
<div hidden="hidden" id="baseUrl">@baseUrl.ToHtmlString()</div> <div hidden="hidden" id="baseUrl">@baseUrl.ToHtmlString()</div>
<head> <head>
<title>@UI.Layout_Title</title> <title>@UI.Layout_Title</title>
<meta charset="utf-8">
<!-- Styles --> <!-- Styles -->
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@Html.LoadAnalytics() @Html.LoadAnalytics()

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14 # Visual Studio 14
VisualStudioVersion = 14.0.25420.1 VisualStudioVersion = 14.0.25123.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.UI", "PlexRequests.UI\PlexRequests.UI.csproj", "{68F5F5F3-B8BB-4911-875F-6F00AAE04EA6}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.UI", "PlexRequests.UI\PlexRequests.UI.csproj", "{68F5F5F3-B8BB-4911-875F-6F00AAE04EA6}"
EndProject EndProject

Loading…
Cancel
Save