This commit is contained in:
tidusjar 2016-10-09 01:13:52 +01:00
parent 9a3eb5fba6
commit 6e3e290359
22 changed files with 766 additions and 34 deletions

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

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

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

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

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

@ -135,7 +135,7 @@
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;"> <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER --> <!-- 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;">This is preheader text. Some clients will show this text as a preview.</span> <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%"> <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 --> <!-- START MAIN CONTENT AREA -->

View file

@ -58,6 +58,7 @@ namespace PlexRequests.Core.SettingModels
public bool Wizard { get; set; } public bool Wizard { get; set; }
public bool DisableTvRequestsByEpisode { get; set; } public bool DisableTvRequestsByEpisode { get; set; }
public bool DisableTvRequestsBySeason { get; set; } public bool DisableTvRequestsBySeason { get; set; }
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;
public interface ISonarrCacher
{ namespace PlexRequests.Services.Interfaces
void Queued(); {
int[] QueuedIds(); public interface ISonarrCacher
} {
} void Queued();
IEnumerable<SonarrCachedResult> QueuedIds();
}
}

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,239 @@
#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
{
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);
}
}
private void Start()
{
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);
}
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)
{
var users = Api.GetUsers(plexSettings.PlexAuthToken);
var settings = EmailSettings.GetSettings();
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!",
};
foreach (var user in users.User)
{
message.Bcc.Add(new MailboxAddress(user.Username, user.Email));
}
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

@ -129,7 +129,7 @@ namespace PlexRequests.Services.Notification
$"Plex Requests: New {model.RequestType.GetString()?.ToLower()} request for {model.Title}!", $"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")}", $"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); model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, }; 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
{ {
@ -150,7 +150,7 @@ namespace PlexRequests.Services.Notification
$"Plex Requests: New issue for {model.Title}!", $"Plex Requests: New issue for {model.Title}!",
$"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!", $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!",
model.ImgSrc); model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, }; 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
{ {
@ -175,7 +175,7 @@ namespace PlexRequests.Services.Notification
$"Plex Requests: {model.Title} is now available!", $"Plex Requests: {model.Title} is now available!",
$"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)", $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)",
model.ImgSrc); model.ImgSrc);
var body = new BodyBuilder { HtmlBody = html, }; 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
{ {

View file

@ -72,6 +72,10 @@
<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" />
@ -79,12 +83,14 @@
<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 +103,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 +143,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

@ -246,7 +246,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

@ -367,7 +367,7 @@ namespace PlexRequests.UI.Modules
viewT.Episodes = dbt.Episodes.ToList(); viewT.Episodes = dbt.Episodes.ToList();
viewT.Approved = dbt.Approved; viewT.Approved = dbt.Approved;
} }
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;
} }
@ -573,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

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

View file

@ -65,17 +65,34 @@
</div> </div>
</div> </div>
<div class="form-group">
<label for="select" class="control-label">Theme</label>
<div id="themes">
<select class="form-control form-control-custom" id="select">
<option @plexTheme class="form-control form-control-custom" value="@Themes.PlexTheme">Plex</option>
<option @originalTheme class="form-control form-control-custom" value="@Themes.OriginalTheme">Original Blue</option>
</select>
</div>
</div>
<br/>
<br/>
<div class="form-group"> <div class="form-group">
<label for="select" class="control-label">Theme</label> <div class="checkbox">
<div id="themes">
<select class="form-control form-control-custom" id="select"> <small>Note: This will require you to setup your email notifications</small>
<option @plexTheme class="form-control form-control-custom" value="@Themes.PlexTheme">Plex</option> @if (Model.SendRecentlyAddedEmail)
<option @originalTheme class="form-control form-control-custom" value="@Themes.OriginalTheme">Original Blue</option> {
</select> <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>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
@if (Model.SearchForMovies) @if (Model.SearchForMovies)