Merge pull request #570 from tidusjar/dev

Dev to master
This commit is contained in:
Jamie 2016-10-09 15:15:05 +01:00 committed by GitHub
commit ce956aaebb
55 changed files with 1980 additions and 305 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,6 +24,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System;
using 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,13 +117,87 @@ 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)
{
season.monitored = true;
}
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)
{
if (e.hasFile || e.seasonNumber != firstSeries.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.SearchForSeason(series.id, firstSeries.seasonNumber, sonarrSettings.ApiKey,
sonarrSettings.FullUri);
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;
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.SearchForSeason(series.id, lastSeries.seasonNumber, sonarrSettings.ApiKey,
sonarrSettings.FullUri);
return new SonarrAddSeries { title = series.title }; // We have updated it
}
if (specificSeasonRequest ?? false)
{ {
// Monitor the seasons that we have chosen // Monitor the seasons that we have chosen
foreach (var season in series.seasons) foreach (var season in series.seasons)
@ -126,44 +205,14 @@ namespace PlexRequests.UI.Helpers
if (model.SeasonList.Contains(season.seasonNumber)) if (model.SeasonList.Contains(season.seasonNumber))
{ {
season.monitored = true; season.monitored = true;
}
}
}
if (requestAll)
{
// Monitor all seasons
foreach (var season in series.seasons)
{
season.monitored = true;
}
}
if (first)
{
var firstSeries = series?.seasons?.OrderBy(x => x.seasonNumber)?.FirstOrDefault() ?? new Season();
firstSeries.monitored = true;
}
if (latest)
{
var lastSeries = series?.seasons?.OrderByDescending(x => x.seasonNumber)?.FirstOrDefault() ?? new Season();
lastSeries.monitored = true;
}
// Update the series in sonarr with the new monitored status
SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri); SonarrApi.UpdateSeries(series, sonarrSettings.ApiKey, sonarrSettings.FullUri);
await RequestAllEpisodesInASeasonWithExistingSeries(model, series, sonarrSettings); SonarrApi.SearchForSeason(series.id, season.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
} }
return null;
var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey,
sonarrSettings.FullUri, true, true);
return result;
} }
public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) 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;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>
<div class="col-md-6 col-md-push-3"><strong>Last Run</strong>
</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>

View file

@ -74,6 +74,26 @@
</select> </select>
</div> </div>
</div> </div>
<br/>
<br/>
<div class="form-group">
<div class="checkbox">
<small>Note: This will require you to setup your email notifications</small>
@if (Model.SendRecentlyAddedEmail)
{
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail" checked="checked"><label for="SendRecentlyAddedEmail">Send out a weekly email of recently added content to all your Plex 'Friends'</label>
}
else
{
<input type="checkbox" id="SendRecentlyAddedEmail" name="SendRecentlyAddedEmail"><label for="SendRecentlyAddedEmail">Send out a weekly email of recently added content to all your Plex 'Friends'</label>
}
</div>
</div>
<button id="recentlyAddedBtn" class="btn btn-primary-outline">Send test email to Admin</button>
<br/>
<br/>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
@ -194,6 +214,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>

View file

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

View file

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

View file

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