Merge pull request #2089 from tidusjar/feature_recentlyAdded

Feature recently added
This commit is contained in:
Jamie 2018-03-22 13:02:16 +00:00 committed by GitHub
commit 5ea81568e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 2289 additions and 49 deletions

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Core.Models; using Ombi.Core.Models;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
@ -10,5 +11,6 @@ namespace Ombi.Core.Engine
IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to); IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to);
IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason); IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason);
IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(bool groupBySeason); IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(bool groupBySeason);
Task<bool> UpdateRecentlyAddedDatabase();
} }
} }

View file

@ -1,27 +1,28 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Ombi.Core.Models; using Ombi.Core.Models;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using RecentlyAddedType = Ombi.Store.Entities.RecentlyAddedType;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
public class RecentlyAddedEngine : IRecentlyAddedEngine public class RecentlyAddedEngine : IRecentlyAddedEngine
{ {
public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby) public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> recentlyAdded)
{ {
_plex = plex; _plex = plex;
_emby = emby; _emby = emby;
_recentlyAddedLog = recentlyAdded;
} }
private readonly IPlexContentRepository _plex; private readonly IPlexContentRepository _plex;
private readonly IEmbyContentRepository _emby; private readonly IEmbyContentRepository _emby;
private readonly IRepository<RecentlyAddedLog> _recentlyAddedLog;
public IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to) public IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to)
{ {
@ -55,6 +56,67 @@ namespace Ombi.Core.Engine
return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason); return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason);
} }
public async Task<bool> UpdateRecentlyAddedDatabase()
{
var plexContent = _plex.GetAll().Include(x => x.Episodes);
var embyContent = _emby.GetAll().Include(x => x.Episodes);
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
foreach (var p in plexContent)
{
if (p.Type == PlexMediaTypeEntity.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = p.Id
});
}
else
{
// Add the episodes
foreach (var ep in p.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = ep.Id
});
}
}
}
foreach (var e in embyContent)
{
if (e.Type == EmbyMediaType.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentId = e.Id
});
}
else
{
// Add the episodes
foreach (var ep in e.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = ep.Id
});
}
}
}
await _recentlyAddedLog.AddRange(recentlyAddedLog);
return true;
}
private IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(IQueryable<PlexServerContent> plexTv, IQueryable<EmbyContent> embyTv, private IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(IQueryable<PlexServerContent> plexTv, IQueryable<EmbyContent> embyTv,
bool groupBySeason) bool groupBySeason)
{ {

View file

@ -0,0 +1,23 @@

using System.Collections.Generic;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.UI
{
/// <summary>
/// The view model for the notification settings page
/// </summary>
/// <seealso cref="NewsletterNotificationViewModel" />
public class NewsletterNotificationViewModel : NewsletterSettings
{
/// <summary>
/// Gets or sets the notification templates.
/// </summary>
/// <value>
/// The notification templates.
/// </value>
public NotificationTemplates NotificationTemplate { get; set; }
}
}

View file

@ -174,6 +174,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IProcessProvider, ProcessProvider>(); services.AddTransient<IProcessProvider, ProcessProvider>();
services.AddTransient<ISickRageSync, SickRageSync>(); services.AddTransient<ISickRageSync, SickRageSync>();
services.AddTransient<IRefreshMetadata, RefreshMetadata>(); services.AddTransient<IRefreshMetadata, RefreshMetadata>();
services.AddTransient<INewsletterJob, NewsletterJob>();
} }
} }
} }

View file

@ -13,5 +13,6 @@
WelcomeEmail = 8, WelcomeEmail = 8,
IssueResolved = 9, IssueResolved = 9,
IssueComment = 10, IssueComment = 10,
Newsletter = 11,
} }
} }

View file

@ -9,5 +9,6 @@
public const string RequestTv = nameof(RequestTv); public const string RequestTv = nameof(RequestTv);
public const string RequestMovie = nameof(RequestMovie); public const string RequestMovie = nameof(RequestMovie);
public const string Disabled = nameof(Disabled); public const string Disabled = nameof(Disabled);
public const string RecievesNewsletter = nameof(RecievesNewsletter);
} }
} }

View file

@ -18,6 +18,7 @@ namespace Ombi.Mapping.Profiles
CreateMap<TelegramNotificationsViewModel, TelegramSettings>().ReverseMap(); CreateMap<TelegramNotificationsViewModel, TelegramSettings>().ReverseMap();
CreateMap<UpdateSettingsViewModel, UpdateSettings>().ReverseMap(); CreateMap<UpdateSettingsViewModel, UpdateSettings>().ReverseMap();
CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap(); CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap();
CreateMap<NewsletterNotificationViewModel, NewsletterSettings>().ReverseMap();
} }
} }
} }

View file

@ -4,19 +4,26 @@ using System.Text;
namespace Ombi.Notifications.Templates namespace Ombi.Notifications.Templates
{ {
public class EmailBasicTemplate : IEmailBasicTemplate public class EmailBasicTemplate : TemplateBase, IEmailBasicTemplate
{ {
public string TemplateLocation public override string TemplateLocation
{ {
get get
{ {
if (string.IsNullOrEmpty(_templateLocation))
{
#if DEBUG #if DEBUG
return Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates", "BasicTemplate.html"); _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates",
"BasicTemplate.html");
#else #else
return Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html"); _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates","BasicTemplate.html");
#endif #endif
} }
return _templateLocation;
} }
}
private string _templateLocation;
private const string SubjectKey = "{@SUBJECT}"; private const string SubjectKey = "{@SUBJECT}";
private const string BodyKey = "{@BODY}"; private const string BodyKey = "{@BODY}";
@ -31,7 +38,7 @@ namespace Ombi.Notifications.Templates
sb.Replace(BodyKey, body); sb.Replace(BodyKey, body);
sb.Replace(DateKey, DateTime.Now.ToString("f")); sb.Replace(DateKey, DateTime.Now.ToString("f"));
sb.Replace(Poster, string.IsNullOrEmpty(imgsrc) ? string.Empty : $"<tr><td align=\"center\"><img src=\"{imgsrc}\" alt=\"Poster\" width=\"400px\" text-align=\"center\"/></td></tr>"); sb.Replace(Poster, string.IsNullOrEmpty(imgsrc) ? string.Empty : $"<tr><td align=\"center\"><img src=\"{imgsrc}\" alt=\"Poster\" width=\"400px\" text-align=\"center\"/></td></tr>");
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? "http://i.imgur.com/qQsN78U.png" : logo); sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo);
return sb.ToString(); return sb.ToString();
} }

View file

@ -0,0 +1,7 @@
namespace Ombi.Notifications.Templates
{
public interface INewsletterTemplate
{
string LoadTemplate(string subject, string intro, string tableHtml, string logo);
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.IO;
using System.Text;
namespace Ombi.Notifications.Templates
{
public class NewsletterTemplate : TemplateBase, INewsletterTemplate
{
public override string TemplateLocation
{
get
{
if (string.IsNullOrEmpty(_templateLocation))
{
#if DEBUG
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp2.0", "Templates", "NewsletterTemplate.html");
#else
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates", "NewsletterTemplate.html");
#endif
}
return _templateLocation;
}
}
private string _templateLocation;
private const string SubjectKey = "{@SUBJECT}";
private const string DateKey = "{@DATENOW}";
private const string Logo = "{@LOGO}";
private const string TableLocation = "{@RECENTLYADDED}";
private const string IntroText = "{@INTRO}";
public string LoadTemplate(string subject, string intro, string tableHtml, string logo)
{
var sb = new StringBuilder(File.ReadAllText(TemplateLocation));
sb.Replace(SubjectKey, subject);
sb.Replace(TableLocation, tableHtml);
sb.Replace(IntroText, intro);
sb.Replace(DateKey, DateTime.Now.ToString("f"));
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo);
return sb.ToString();
}
}
}

View file

@ -9,6 +9,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Update="Templates\NewsletterTemplate.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Templates\BasicTemplate.html"> <None Update="Templates\BasicTemplate.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>

View file

@ -0,0 +1,8 @@
namespace Ombi.Notifications.Templates
{
public abstract class TemplateBase
{
public abstract string TemplateLocation { get; }
public virtual string OmbiLogo => "http://i.imgur.com/qQsN78U.png";
}
}

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>Ombi</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;">Ombi 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="{@LOGO}" 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!</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/Ombi" style="color: #999999; font-size: 12px; text-align: center; text-decoration: underline;">Ombi</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

@ -107,9 +107,13 @@ namespace Ombi.Notifications
var body = new BodyBuilder var body = new BodyBuilder
{ {
HtmlBody = model.Message, HtmlBody = model.Message,
TextBody = model.Other["PlainTextBody"]
}; };
if (model.Other.ContainsKey("PlainTextBody"))
{
body.TextBody = model.Other["PlainTextBody"];
}
var message = new MimeMessage var message = new MimeMessage
{ {
Body = body.ToMessageBody(), Body = body.ToMessageBody(),

View file

@ -38,6 +38,13 @@ namespace Ombi.Notifications
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty; AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
} }
public void SetupNewsletter(CustomizationSettings s, string username)
{
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = username;
}
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s) public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s)
{ {
LoadIssues(opts); LoadIssues(opts);

View file

@ -17,7 +17,8 @@ namespace Ombi.Schedule
public JobSetup(IPlexContentSync plexContentSync, IRadarrSync radarrSync, public JobSetup(IPlexContentSync plexContentSync, IRadarrSync radarrSync,
IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter, IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter,
IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache, IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache,
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh) ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
INewsletterJob newsletter)
{ {
_plexContentSync = plexContentSync; _plexContentSync = plexContentSync;
_radarrSync = radarrSync; _radarrSync = radarrSync;
@ -30,6 +31,7 @@ namespace Ombi.Schedule
_jobSettings = jobsettings; _jobSettings = jobsettings;
_srSync = srSync; _srSync = srSync;
_refreshMetadata = refresh; _refreshMetadata = refresh;
_newsletter = newsletter;
} }
private readonly IPlexContentSync _plexContentSync; private readonly IPlexContentSync _plexContentSync;
@ -43,6 +45,7 @@ namespace Ombi.Schedule
private readonly ISickRageSync _srSync; private readonly ISickRageSync _srSync;
private readonly ISettingsService<JobSettings> _jobSettings; private readonly ISettingsService<JobSettings> _jobSettings;
private readonly IRefreshMetadata _refreshMetadata; private readonly IRefreshMetadata _refreshMetadata;
private readonly INewsletterJob _newsletter;
public void Setup() public void Setup()
{ {
@ -60,6 +63,7 @@ namespace Ombi.Schedule
RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s)); RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s));
RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s)); RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s));
BackgroundJob.Enqueue(() => _newsletter.Start());
} }
} }
} }

View file

@ -0,0 +1,46 @@
using System.Text;
namespace Ombi.Schedule.Jobs.Ombi
{
public abstract class HtmlTemplateGenerator
{
protected virtual void AddParagraph(StringBuilder stringBuilder, string text, int fontSize = 14, string fontWeight = "normal")
{
stringBuilder.AppendFormat("<p style=\"font-family: sans-serif; font-size: {1}px; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{0}</p>", text, fontSize, fontWeight);
}
protected virtual void AddImageInsideTable(StringBuilder sb, string url, int size = 400)
{
sb.Append("<tr>");
sb.Append("<td align=\"center\">");
sb.Append($"<img src=\"{url}\" width=\"{size}px\" text-align=\"center\" />");
sb.Append("</td>");
sb.Append("</tr>");
}
protected virtual void Href(StringBuilder sb, string url)
{
sb.AppendFormat("<a href=\"{0}\">", url);
}
protected virtual void TableData(StringBuilder sb)
{
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
}
protected virtual void EndTag(StringBuilder sb, string tag)
{
sb.AppendFormat("</{0}>", tag);
}
protected virtual void Header(StringBuilder sb, int size, string text, string fontWeight = "normal")
{
sb.AppendFormat(
"<h{0} style=\"font-family: sans-serif; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{1}</h{0}>",
size, text, fontWeight);
}
}
}

View file

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Ombi.Schedule.Jobs.Ombi
{
public interface INewsletterJob : IBaseJob
{
Task Start();
}
}

View file

@ -0,0 +1,563 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.TvMaze;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications;
using Ombi.Notifications.Models;
using Ombi.Notifications.Templates;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Schedule.Jobs.Ombi
{
public class NewsletterJob : HtmlTemplateGenerator, INewsletterJob
{
public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> addedLog,
IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService<CustomizationSettings> custom,
ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo,
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter)
{
_plex = plex;
_emby = emby;
_recentlyAddedLog = addedLog;
_movieApi = movieApi;
_tvApi = tvApi;
_email = email;
_customizationSettings = custom;
_templateRepo = templateRepo;
_emailSettings = emailSettings;
_newsletterSettings = newsletter;
_userManager = um;
}
private readonly IPlexContentRepository _plex;
private readonly IEmbyContentRepository _emby;
private readonly IRepository<RecentlyAddedLog> _recentlyAddedLog;
private readonly IMovieDbApi _movieApi;
private readonly ITvMazeApi _tvApi;
private readonly IEmailProvider _email;
private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly INotificationTemplatesRepository _templateRepo;
private readonly ISettingsService<EmailNotificationSettings> _emailSettings;
private readonly ISettingsService<NewsletterSettings> _newsletterSettings;
private readonly UserManager<OmbiUser> _userManager;
public async Task Start()
{
var newsletterSettings = await _newsletterSettings.GetSettingsAsync();
if (!newsletterSettings.Enabled)
{
return;
}
var template = await _templateRepo.GetTemplate(NotificationAgent.Email, NotificationType.Newsletter);
if (!template.Enabled)
{
return;
}
var emailSettings = await _emailSettings.GetSettingsAsync();
if (!ValidateConfiguration(emailSettings))
{
return;
}
var customization = await _customizationSettings.GetSettingsAsync();
// Get the Content
var plexContent = _plex.GetAll().Include(x => x.Episodes);
var embyContent = _emby.GetAll().Include(x => x.Episodes);
var addedLog = _recentlyAddedLog.GetAll();
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId);
var addedPlexEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode).Select(x => x.ContentId);
var addedEmbyEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Episode).Select(x => x.ContentId);
// Filter out the ones that we haven't sent yet
var plexContentMoviesToSend = plexContent.Where(x => !addedPlexMovieLogIds.Contains(x.Id));
var embyContentMoviesToSend = embyContent.Where(x => !addedEmbyMoviesLogIds.Contains(x.Id));
var plexContentTvToSend = plexContent.Where(x => x.Episodes.Any(e => !addedPlexEpisodesLogIds.Contains(e.Id)));
var embyContentTvToSend = embyContent.Where(x => x.Episodes.Any(e => !addedEmbyEpisodesLogIds.Contains(e.Id)));
var plexContentToSend = plexContentMoviesToSend.Union(plexContentTvToSend);
var embyContentToSend = embyContentMoviesToSend.Union(embyContentTvToSend);
var body = await BuildHtml(plexContentToSend, embyContentToSend);
// Get the users to send it to
var users = await _userManager.GetUsersInRoleAsync(OmbiRoles.RecievesNewsletter);
if (!users.Any())
{
return;
}
var emailTasks = new List<Task>();
foreach (var user in users)
{
if (user.Email.IsNullOrEmpty())
{
continue;
}
var html = LoadTemplate(body, template, customization, user.Alias);
emailTasks.Add(_email.Send(new NotificationMessage { Message = html, Subject = template.Subject, To = user.Email }, emailSettings));
}
// Now add all of this to the Recently Added log
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
foreach (var p in plexContentMoviesToSend)
{
if (p.Type == PlexMediaTypeEntity.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = p.Id
});
}
else
{
// Add the episodes
foreach (var ep in p.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = ep.Id
});
}
}
}
foreach (var e in embyContentMoviesToSend)
{
if (e.Type == EmbyMediaType.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentId = e.Id
});
}
else
{
// Add the episodes
foreach (var ep in e.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = ep.Id
});
}
}
}
await _recentlyAddedLog.AddRange(recentlyAddedLog);
await Task.WhenAll(emailTasks.ToArray());
}
private string LoadTemplate(string body, NotificationTemplates template, CustomizationSettings settings, string username)
{
var email = new NewsletterTemplate();
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
curlys.SetupNewsletter(settings, username);
var parsed = resolver.ParseMessage(template, curlys);
var html = email.LoadTemplate(parsed.Subject, parsed.Message, body, settings.Logo);
return html;
}
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend)
{
var sb = new StringBuilder();
var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie);
var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie);
if (plexMovies.Any() || embyMovies.Any())
{
sb.Append("<h1>New Movies:</h1><br /><br />");
await ProcessPlexMovies(plexMovies, sb);
await ProcessEmbyMovies(embyMovies, sb);
}
var plexTv = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Show);
var embyTv = embyContentToSend.Where(x => x.Type == EmbyMediaType.Series);
if (plexTv.Any() || embyTv.Any())
{
sb.Append("<h1>New Episodes:</h1><br /><br />");
await ProcessPlexTv(plexTv, sb);
await ProcessEmbyMovies(embyTv, sb);
}
return sb.ToString();
}
private async Task ProcessPlexMovies(IQueryable<PlexServerContent> plexContentToSend, StringBuilder sb)
{
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%\">");
var ordered = plexContentToSend.OrderByDescending(x => x.AddedAt);
foreach (var content in ordered)
{
if (content.TheMovieDbId.IsNullOrEmpty())
{
// Maybe we should try the ImdbId?
if (content.ImdbId.HasValue())
{
var findResult = await _movieApi.Find(content.ImdbId, ExternalSource.imdb_id);
var movieId = findResult.movie_results?[0]?.id ?? 0;
content.TheMovieDbId = movieId.ToString();
}
}
int.TryParse(content.TheMovieDbId, out var movieDbId);
var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId);
if (info == null)
{
continue;
}
try
{
AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.BackdropPath}");
sb.Append("<tr>");
TableData(sb);
Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/");
Header(sb, 3, $"{info.Title} {info.ReleaseDate ?? string.Empty}");
EndTag(sb, "a");
if (info.Genres.Any())
{
AddParagraph(sb,
$"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}");
}
AddParagraph(sb, info.Overview);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
finally
{
EndLoopHtml(sb);
}
}
}
private async Task ProcessEmbyMovies(IQueryable<EmbyContent> embyContent, StringBuilder sb)
{
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%\">");
var ordered = embyContent.OrderByDescending(x => x.AddedAt);
foreach (var content in ordered)
{
int.TryParse(content.ProviderId, out var movieDbId);
var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId);
if (info == null)
{
continue;
}
try
{
AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.BackdropPath}");
sb.Append("<tr>");
TableData(sb);
Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/");
Header(sb, 3, $"{info.Title} {info.ReleaseDate ?? string.Empty}");
EndTag(sb, "a");
if (info.Genres.Any())
{
AddParagraph(sb,
$"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}");
}
AddParagraph(sb, info.Overview);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
finally
{
EndLoopHtml(sb);
}
}
}
private async Task ProcessPlexTv(IQueryable<PlexServerContent> plexContent, StringBuilder sb)
{
var orderedTv = plexContent.OrderByDescending(x => x.AddedAt);
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 orderedTv)
{
try
{
if (!t.HasTvDb)
{
// We may need to use themoviedb for the imdbid or their own id to get info
if (t.HasTheMovieDb)
{
int.TryParse(t.TheMovieDbId, out var movieId);
var externals = await _movieApi.GetTvExternals(movieId);
if (externals == null || externals.tvdb_id <= 0)
{
continue;
}
t.TvDbId = externals.tvdb_id.ToString();
}
// WE could check the below but we need to get the moviedb and then perform the above, let the metadata job figure this out.
//else if(t.HasImdb)
//{
// // Check the imdbid
// var externals = await _movieApi.Find(t.ImdbId, ExternalSource.imdb_id);
// if (externals?.tv_results == null || externals.tv_results.Length <= 0)
// {
// continue;
// }
// t.TvDbId = externals.tv_results.FirstOrDefault()..ToString();
//}
}
int.TryParse(t.TvDbId, out var tvdbId);
var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId);
if (info == null)
{
continue;
}
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
}
AddImageInsideTable(sb, banner);
sb.Append("<tr>");
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
var title = $"{t.Title} {t.ReleaseYear}";
Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/");
Header(sb, 3, title);
EndTag(sb, "a");
// Group by the season number
var results = t.Episodes?.GroupBy(p => p.SeasonNumber,
(key, g) => new
{
SeasonNumber = key,
Episodes = g.ToList()
}
);
// Group the episodes
foreach (var epInformation in results.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var epSb = new StringBuilder();
for (var i = 0; i < orderedEpisodes.Count; i++)
{
var ep = orderedEpisodes[i];
if (i < orderedEpisodes.Count - 1)
{
epSb.Append($"{ep.EpisodeNumber},");
}
else
{
epSb.Append($"{ep.EpisodeNumber}");
}
}
AddParagraph(sb, $"Season: {epInformation.SeasonNumber}, Episode: {epSb}");
}
if (info.genres.Any())
{
AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}");
}
AddParagraph(sb, info.summary);
}
catch (Exception e)
{
//Log.Error(e);
}
finally
{
EndLoopHtml(sb);
}
}
sb.Append("</table><br /><br />");
}
private async Task ProcessEmbyTv(IQueryable<EmbyContent> plexContent, StringBuilder sb)
{
var orderedTv = plexContent.OrderByDescending(x => x.AddedAt);
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 orderedTv)
{
try
{
int.TryParse(t.ProviderId, out var tvdbId);
var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId);
if (info == null)
{
continue;
}
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
}
AddImageInsideTable(sb, banner);
sb.Append("<tr>");
sb.Append(
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/");
Header(sb, 3, t.Title);
EndTag(sb, "a");
// Group by the season number
var results = t.Episodes?.GroupBy(p => p.SeasonNumber,
(key, g) => new
{
SeasonNumber = key,
Episodes = g.ToList()
}
);
// Group the episodes
foreach (var epInformation in results.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var epSb = new StringBuilder();
for (var i = 0; i < orderedEpisodes.Count; i++)
{
var ep = orderedEpisodes[i];
if (i < orderedEpisodes.Count - 1)
{
epSb.Append($"{ep.EpisodeNumber},");
}
else
{
epSb.Append($"{ep.EpisodeNumber}");
}
}
AddParagraph(sb, $"Season: {epInformation.SeasonNumber}, Episode: {epSb}");
}
if (info.genres.Any())
{
AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}");
}
AddParagraph(sb, info.summary);
}
catch (Exception e)
{
//Log.Error(e);
}
finally
{
EndLoopHtml(sb);
}
}
sb.Append("</table><br /><br />");
}
private void EndLoopHtml(StringBuilder sb)
{
//NOTE: BR have to be in TD's as per html spec or it will be put outside of the table...
//Source: http://stackoverflow.com/questions/6588638/phantom-br-tag-rendered-by-browsers-prior-to-table-tag
sb.Append("<hr />");
sb.Append("<br />");
sb.Append("<br />");
sb.Append("</td>");
sb.Append("</tr>");
}
protected bool ValidateConfiguration(EmailNotificationSettings settings)
{
if (!settings.Enabled)
{
return false;
}
if (settings.Authentication)
{
if (string.IsNullOrEmpty(settings.Username) || string.IsNullOrEmpty(settings.Password))
{
return false;
}
}
if (string.IsNullOrEmpty(settings.Host) || string.IsNullOrEmpty(settings.AdminEmail) || string.IsNullOrEmpty(settings.Port.ToString()))
{
return false;
}
return true;
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_plex?.Dispose();
_emby?.Dispose();
_newsletterSettings?.Dispose();
_customizationSettings?.Dispose();
_emailSettings.Dispose();
_recentlyAddedLog.Dispose();
_templateRepo?.Dispose();
_userManager?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View file

@ -220,12 +220,6 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
private async Task StartEmby()
{
}
private bool _disposed; private bool _disposed;
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
@ -236,6 +230,7 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
_plexRepo?.Dispose(); _plexRepo?.Dispose();
_embyRepo?.Dispose(); _embyRepo?.Dispose();
_plexSettings?.Dispose();
} }
_disposed = true; _disposed = true;
} }

View file

@ -18,6 +18,7 @@ namespace Ombi.Settings.Settings.Models
public string PresetThemeName { get; set; } public string PresetThemeName { get; set; }
public string PresetThemeContent { get; set; } public string PresetThemeContent { get; set; }
public bool RecentlyAddedPage { get; set; }
[NotMapped] [NotMapped]
public string PresetThemeVersion public string PresetThemeVersion

View file

@ -0,0 +1,7 @@
namespace Ombi.Settings.Settings.Models.Notifications
{
public class NewsletterSettings : Settings
{
public bool Enabled { get; set; }
}
}

View file

@ -41,5 +41,6 @@ namespace Ombi.Store.Context
DbSet<SickRageCache> SickRageCache { get; set; } DbSet<SickRageCache> SickRageCache { get; set; }
DbSet<SickRageEpisodeCache> SickRageEpisodeCache { get; set; } DbSet<SickRageEpisodeCache> SickRageEpisodeCache { get; set; }
DbSet<RequestLog> RequestLogs { get; set; } DbSet<RequestLog> RequestLogs { get; set; }
DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
} }
} }

View file

@ -37,6 +37,7 @@ namespace Ombi.Store.Context
public DbSet<IssueCategory> IssueCategories { get; set; } public DbSet<IssueCategory> IssueCategories { get; set; }
public DbSet<IssueComments> IssueComments { get; set; } public DbSet<IssueComments> IssueComments { get; set; }
public DbSet<RequestLog> RequestLogs { get; set; } public DbSet<RequestLog> RequestLogs { get; set; }
public DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
public DbSet<Audit> Audit { get; set; } public DbSet<Audit> Audit { get; set; }
@ -218,6 +219,16 @@ namespace Ombi.Store.Context
break; break;
case NotificationType.AdminNote: case NotificationType.AdminNote:
continue; continue;
case NotificationType.Newsletter:
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
Message = "Here is a list of Movies and TV Shows that have recently been added!",
Subject = "{ApplicationName}: Recently Added Content!",
Agent = agent,
Enabled = true,
};
break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }

View file

@ -56,6 +56,15 @@ namespace Ombi.Store.Entities
public int Key { get; set; } public int Key { get; set; }
public DateTime AddedAt { get; set; } public DateTime AddedAt { get; set; }
public string Quality { get; set; } public string Quality { get; set; }
[NotMapped]
public bool HasImdb => !string.IsNullOrEmpty(ImdbId);
[NotMapped]
public bool HasTvDb => !string.IsNullOrEmpty(TvDbId);
[NotMapped]
public bool HasTheMovieDb => !string.IsNullOrEmpty(TheMovieDbId);
} }
[Table("PlexSeasonsContent")] [Table("PlexSeasonsContent")]

View file

@ -0,0 +1,26 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities
{
[Table("RecentlyAddedLog")]
public class RecentlyAddedLog : Entity
{
public RecentlyAddedType Type { get; set; }
public ContentType ContentType { get; set; }
public int ContentId { get; set; } // This is dependant on the type
public DateTime AddedAt { get; set; }
}
public enum RecentlyAddedType
{
Plex = 0,
Emby = 1
}
public enum ContentType
{
Parent = 0,
Episode = 1
}
}

View file

@ -0,0 +1,934 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Ombi.Helpers;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using System;
namespace Ombi.Store.Migrations
{
[DbContext(typeof(OmbiContext))]
[Migration("20180322085345_RecentlyAddedLog")]
partial class RecentlyAddedLog
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Type");
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("ApplicationConfiguration");
});
modelBuilder.Entity("Ombi.Store.Entities.Audit", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AuditArea");
b.Property<int>("AuditType");
b.Property<DateTime>("DateTime");
b.Property<string>("Description");
b.Property<string>("User");
b.HasKey("Id");
b.ToTable("Audit");
});
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("CouchPotatoCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId")
.IsRequired();
b.Property<string>("ProviderId");
b.Property<string>("Title");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId");
b.Property<int>("EpisodeNumber");
b.Property<string>("ParentId");
b.Property<string>("ProviderId");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Content");
b.Property<string>("SettingsName");
b.HasKey("Id");
b.ToTable("GlobalSettings");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Agent");
b.Property<bool>("Enabled");
b.Property<string>("Message");
b.Property<int>("NotificationType");
b.Property<string>("Subject");
b.HasKey("Id");
b.ToTable("NotificationTemplates");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("PlayerId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("NotificationUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("Alias");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<string>("EmbyConnectUserId");
b.Property<int?>("EpisodeRequestLimit");
b.Property<DateTime?>("LastLoggedIn");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<int?>("MovieRequestLimit");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("ProviderUserId");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserAccessToken");
b.Property<string>("UserName")
.HasMaxLength(256);
b.Property<int>("UserType");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("GrandparentKey");
b.Property<int>("Key");
b.Property<int>("ParentKey");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ParentKey");
b.Property<int>("PlexContentId");
b.Property<int?>("PlexServerContentId");
b.Property<int>("SeasonKey");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("PlexServerContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("ImdbId");
b.Property<int>("Key");
b.Property<string>("Quality");
b.Property<string>("ReleaseYear");
b.Property<string>("TheMovieDbId");
b.Property<string>("Title");
b.Property<string>("TvDbId");
b.Property<int>("Type");
b.Property<string>("Url");
b.HasKey("Id");
b.ToTable("PlexServerContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("HasFile");
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<int>("ContentId");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("RecentlyAddedLog");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<int?>("IssueId");
b.Property<int>("ParentRequestId");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("SeriesType");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentRequestId");
b.HasIndex("RequestedUserId");
b.ToTable("ChildRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("IssueCategory");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Comment");
b.Property<DateTime>("Date");
b.Property<int?>("IssuesId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("IssuesId");
b.HasIndex("UserId");
b.ToTable("IssueComments");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int>("IssueCategoryId");
b.Property<int?>("IssueId");
b.Property<string>("ProviderId");
b.Property<int?>("RequestId");
b.Property<int>("RequestType");
b.Property<DateTime?>("ResovledDate");
b.Property<int>("Status");
b.Property<string>("Subject");
b.Property<string>("Title");
b.Property<string>("UserReportedId");
b.HasKey("Id");
b.HasIndex("IssueCategoryId");
b.HasIndex("IssueId");
b.HasIndex("UserReportedId");
b.ToTable("Issues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<string>("Background");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<DateTime?>("DigitalReleaseDate");
b.Property<string>("ImdbId");
b.Property<int?>("IssueId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<int>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("RootPathOverride");
b.Property<string>("Status");
b.Property<int>("TheMovieDbId");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("RequestedUserId");
b.ToTable("MovieRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeCount");
b.Property<DateTime>("RequestDate");
b.Property<int>("RequestId");
b.Property<int>("RequestType");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RequestLog");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ImdbId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<int?>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int?>("RootFolder");
b.Property<string>("Status");
b.Property<string>("Title");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("TvRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<bool>("HasFile");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Token");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Tokens");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AirDate");
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<int>("EpisodeNumber");
b.Property<bool>("Requested");
b.Property<int>("SeasonId");
b.Property<string>("Title");
b.Property<string>("Url");
b.HasKey("Id");
b.HasIndex("SeasonId");
b.ToTable("EpisodeRequests");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ChildRequestId");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("ChildRequestId");
b.ToTable("SeasonRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("NotificationUserIds")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent")
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest")
.WithMany("ChildRequests")
.HasForeignKey("ParentRequestId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues")
.WithMany("Comments")
.HasForeignKey("IssuesId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory")
.WithMany()
.HasForeignKey("IssueCategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported")
.WithMany()
.HasForeignKey("UserReportedId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest")
.WithMany("SeasonRequests")
.HasForeignKey("ChildRequestId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class RecentlyAddedLog : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "RecentlyAddedLog",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
AddedAt = table.Column<DateTime>(nullable: false),
ContentId = table.Column<int>(nullable: false),
Type = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RecentlyAddedLog", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "RecentlyAddedLog");
}
}
}

View file

@ -20,7 +20,7 @@ namespace Ombi.Store.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); .HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{ {
@ -430,6 +430,22 @@ namespace Ombi.Store.Migrations
b.ToTable("RadarrCache"); b.ToTable("RadarrCache");
}); });
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<int>("ContentId");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("RecentlyAddedLog");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Helpers; using Ombi.Helpers;
@ -6,11 +7,11 @@ using Ombi.Store.Entities;
namespace Ombi.Store.Repository namespace Ombi.Store.Repository
{ {
public interface INotificationTemplatesRepository public interface INotificationTemplatesRepository : IDisposable
{ {
IQueryable<NotificationTemplates> All(); IQueryable<NotificationTemplates> All();
Task<IEnumerable<NotificationTemplates>> GetAllTemplates(); IQueryable<NotificationTemplates> GetAllTemplates();
Task<IEnumerable<NotificationTemplates>> GetAllTemplates(NotificationAgent agent); IQueryable<NotificationTemplates> GetAllTemplates(NotificationAgent agent);
Task<NotificationTemplates> Insert(NotificationTemplates entity); Task<NotificationTemplates> Insert(NotificationTemplates entity);
Task Update(NotificationTemplates template); Task Update(NotificationTemplates template);
Task UpdateRange(IEnumerable<NotificationTemplates> template); Task UpdateRange(IEnumerable<NotificationTemplates> template);

View file

@ -23,14 +23,14 @@ namespace Ombi.Store.Repository
return Db.NotificationTemplates.AsQueryable(); return Db.NotificationTemplates.AsQueryable();
} }
public async Task<IEnumerable<NotificationTemplates>> GetAllTemplates() public IQueryable<NotificationTemplates> GetAllTemplates()
{ {
return await Db.NotificationTemplates.ToListAsync(); return Db.NotificationTemplates;
} }
public async Task<IEnumerable<NotificationTemplates>> GetAllTemplates(NotificationAgent agent) public IQueryable<NotificationTemplates> GetAllTemplates(NotificationAgent agent)
{ {
return await Db.NotificationTemplates.Where(x => x.Agent == agent).ToListAsync(); return Db.NotificationTemplates.Where(x => x.Agent == agent);
} }
public async Task<NotificationTemplates> GetTemplate(NotificationAgent agent, NotificationType type) public async Task<NotificationTemplates> GetTemplate(NotificationAgent agent, NotificationType type)
@ -60,5 +60,26 @@ namespace Ombi.Store.Repository
await Db.SaveChangesAsync().ConfigureAwait(false); await Db.SaveChangesAsync().ConfigureAwait(false);
return settings.Entity; return settings.Entity;
} }
private bool _disposed;
// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
Db?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
} }
} }

View file

@ -34,13 +34,14 @@
<i class="fa fa-th-list"></i> {{ 'NavigationBar.Requests' | translate }}</a> <i class="fa fa-th-list"></i> {{ 'NavigationBar.Requests' | translate }}</a>
</li> </li>
</ul> </ul>
<div *ngIf="customizationSettings">
<ul class="nav navbar-nav"> <ul *ngIf="customizationSettings.recentlyAddedPage" class="nav navbar-nav">
<li id="Requests" [routerLinkActive]="['active']"> <li id="RecentlyAdded" [routerLinkActive]="['active']">
<a [routerLink]="['/recentlyadded']"> <a [routerLink]="['/recentlyadded']">
<i class="fa fa-check"></i> {{ 'NavigationBar.RecentlyAdded' | translate }}</a> <i class="fa fa-check"></i> {{ 'NavigationBar.RecentlyAdded' | translate }}</a>
</li> </li>
</ul> </ul>
</div>
<ul *ngIf="issuesEnabled" class="nav navbar-nav"> <ul *ngIf="issuesEnabled" class="nav navbar-nav">
<li id="Issues" [routerLinkActive]="['active']"> <li id="Issues" [routerLinkActive]="['active']">
<a [routerLink]="['/issues']"> <a [routerLink]="['/issues']">

View file

@ -46,6 +46,7 @@ export enum NotificationType {
WelcomeEmail = 8, WelcomeEmail = 8,
IssueResolved = 9, IssueResolved = 9,
IssueComment = 10, IssueComment = 10,
Newsletter = 11,
} }
export interface IDiscordNotifcationSettings extends INotificationSettings { export interface IDiscordNotifcationSettings extends INotificationSettings {
@ -54,6 +55,10 @@ export interface IDiscordNotifcationSettings extends INotificationSettings {
notificationTemplates: INotificationTemplates[]; notificationTemplates: INotificationTemplates[];
} }
export interface INewsletterNotificationSettings extends INotificationSettings {
notificationTemplate: INotificationTemplates;
}
export interface ITelegramNotifcationSettings extends INotificationSettings { export interface ITelegramNotifcationSettings extends INotificationSettings {
botApi: string; botApi: string;
chatId: string; chatId: string;

View file

@ -107,6 +107,7 @@ export interface ICustomizationSettings extends ISettings {
presetThemeContent: string; presetThemeContent: string;
presetThemeDisplayName: string; presetThemeDisplayName: string;
presetThemeVersion: string; presetThemeVersion: string;
recentlyAddedPage: boolean;
} }
export interface IThemes { export interface IThemes {

View file

@ -20,6 +20,7 @@ import {
ILandingPageSettings, ILandingPageSettings,
IMattermostNotifcationSettings, IMattermostNotifcationSettings,
IMobileNotifcationSettings, IMobileNotifcationSettings,
INewsletterNotificationSettings,
IOmbiSettings, IOmbiSettings,
IPlexSettings, IPlexSettings,
IPushbulletNotificationSettings, IPushbulletNotificationSettings,
@ -265,4 +266,17 @@ export class SettingsService extends ServiceHelpers {
return this.http return this.http
.post<boolean>(`${this.url}/issues`, JSON.stringify(settings), {headers: this.headers}); .post<boolean>(`${this.url}/issues`, JSON.stringify(settings), {headers: this.headers});
} }
public getNewsletterSettings(): Observable<INewsletterNotificationSettings> {
return this.http.get<INewsletterNotificationSettings>(`${this.url}/notifications/newsletter`, {headers: this.headers});
}
public updateNewsletterDatabase(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/notifications/newsletterdatabase`, {headers: this.headers});
}
public saveNewsletterSettings(settings: INewsletterNotificationSettings): Observable<boolean> {
return this.http
.post<boolean>(`${this.url}/notifications/newsletter`, JSON.stringify(settings), {headers: this.headers});
}
} }

View file

@ -33,6 +33,13 @@
</div> </div>
</div> </div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enable" [(ngModel)]="settings.recentlyAddedPage" [checked]="settings.recentlyAddedPage">
<label for="enable">Enable Recently Added Page</label>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="logo" class="control-label">Custom Logo</label> <label for="logo" class="control-label">Custom Logo</label>
<div> <div>

View file

@ -0,0 +1,52 @@
<settings-menu></settings-menu>
<div *ngIf="form">
<fieldset>
<legend>Newsletter</legend>
<div class="col-md-6">
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enable" formControlName="enabled">
<label for="enable">Enabled</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enabled" [(ngModel)]="template.enabled" ng-checked="template.enabled">
<label for="enabled">Enable</label>
</div>
</div>
<div class="form-group" *ngIf="showSubject">
<label class="control-label">Subject</label>
<div>
<input type="text" class="form-control form-control-custom" [(ngModel)]="template.subject" value="{{template.subject}}">
</div>
</div>
<div class="form-group">
<label class="control-label">Message</label>
<div>
<textarea type="text" class="form-control form-control-custom" [(ngModel)]="template.message" value="{{template.message}}"></textarea>
</div>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
<button [disabled]="form.invalid" type="button" (click)="updateDatabase()" class="btn btn-info-outline" tooltipPosition="top" pTooltip="I reccomend running this with a fresh Ombi install, this will set all the current *found* content to have been sent via Newsletter,
if you do not do this then eventhing that Ombi has found in your libraries will go out on the first email!">Update Database</button>
</div>
</div>
</form>
</div>
<div class="col-md-6">
</div>
</fieldset>
</div>

View file

@ -0,0 +1,71 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { INewsletterNotificationSettings, INotificationTemplates, NotificationType } from "../../interfaces";
import { TesterService } from "../../services";
import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
@Component({
templateUrl: "./newsletter.component.html",
})
export class NewsletterComponent implements OnInit {
public NotificationType = NotificationType;
public template: INotificationTemplates;
public form: FormGroup;
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
private fb: FormBuilder,
private testerService: TesterService) { }
public ngOnInit() {
this.settingsService.getNewsletterSettings().subscribe(x => {
this.template = x.notificationTemplate;
this.form = this.fb.group({
enabled: [x.enabled],
});
});
}
public updateDatabase() {
this.settingsService.updateNewsletterDatabase().subscribe();
}
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
const settings = <INewsletterNotificationSettings>form.value;
settings.notificationTemplate = this.template;
this.settingsService.saveNewsletterSettings(settings).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved the Newsletter settings");
} else {
this.notificationService.error("There was an error when saving the Newsletter settings");
}
});
}
public test(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
this.testerService.discordTest(form.value).subscribe(x => {
if (x) {
this.notificationService.success("Successfully sent a Discord message, please check the discord channel");
} else {
this.notificationService.error("There was an error when sending the Discord message. Please check your settings");
}
});
}
}

View file

@ -25,6 +25,7 @@ import { DiscordComponent } from "./notifications/discord.component";
import { EmailNotificationComponent } from "./notifications/emailnotification.component"; import { EmailNotificationComponent } from "./notifications/emailnotification.component";
import { MattermostComponent } from "./notifications/mattermost.component"; import { MattermostComponent } from "./notifications/mattermost.component";
import { MobileComponent } from "./notifications/mobile.component"; import { MobileComponent } from "./notifications/mobile.component";
import { NewsletterComponent } from "./notifications/newsletter.component";
import { NotificationTemplate } from "./notifications/notificationtemplate.component"; import { NotificationTemplate } from "./notifications/notificationtemplate.component";
import { PushbulletComponent } from "./notifications/pushbullet.component"; import { PushbulletComponent } from "./notifications/pushbullet.component";
import { PushoverComponent } from "./notifications/pushover.component"; import { PushoverComponent } from "./notifications/pushover.component";
@ -43,6 +44,7 @@ import { SettingsMenuComponent } from "./settingsmenu.component";
import { AutoCompleteModule, CalendarModule, DialogModule, InputSwitchModule, InputTextModule, MenuModule, RadioButtonModule, TooltipModule } from "primeng/primeng"; import { AutoCompleteModule, CalendarModule, DialogModule, InputSwitchModule, InputTextModule, MenuModule, RadioButtonModule, TooltipModule } from "primeng/primeng";
const routes: Routes = [ const routes: Routes = [
{ path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] }, { path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] },
{ path: "About", component: AboutComponent, canActivate: [AuthGuard] }, { path: "About", component: AboutComponent, canActivate: [AuthGuard] },
@ -69,6 +71,7 @@ const routes: Routes = [
{ path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] }, { path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] },
{ path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] }, { path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] },
{ path: "MassEmail", component: MassEmailComponent, canActivate: [AuthGuard] }, { path: "MassEmail", component: MassEmailComponent, canActivate: [AuthGuard] },
{ path: "Newsletter", component: NewsletterComponent, canActivate: [AuthGuard] },
]; ];
@NgModule({ @NgModule({
@ -119,6 +122,7 @@ const routes: Routes = [
AuthenticationComponent, AuthenticationComponent,
MobileComponent, MobileComponent,
MassEmailComponent, MassEmailComponent,
NewsletterComponent,
], ],
exports: [ exports: [
RouterModule, RouterModule,

View file

@ -56,7 +56,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Email']">Email</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Email']">Email</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/MassEmail']">Mass Email</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/MassEmail']">Mass Email</a></li>
<!--<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Newsletter']">Newsletter</a></li>--> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Newsletter']">Newsletter</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Discord']">Discord</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Discord']">Discord</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Slack']">Slack</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Slack']">Slack</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Pushbullet']">Pushbullet</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Pushbullet']">Pushbullet</a></li>

View file

@ -32,6 +32,7 @@ using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Ombi.Api.Github; using Ombi.Api.Github;
using Ombi.Core.Engine;
namespace Ombi.Controllers namespace Ombi.Controllers
{ {
@ -60,7 +61,8 @@ namespace Ombi.Controllers
IEmbyApi embyApi, IEmbyApi embyApi,
IRadarrSync radarrSync, IRadarrSync radarrSync,
ICacheService memCache, ICacheService memCache,
IGithubApi githubApi) IGithubApi githubApi,
IRecentlyAddedEngine engine)
{ {
SettingsResolver = resolver; SettingsResolver = resolver;
Mapper = mapper; Mapper = mapper;
@ -78,6 +80,7 @@ namespace Ombi.Controllers
private readonly IRadarrSync _radarrSync; private readonly IRadarrSync _radarrSync;
private readonly ICacheService _cache; private readonly ICacheService _cache;
private readonly IGithubApi _githubApi; private readonly IGithubApi _githubApi;
private readonly IRecentlyAddedEngine _recentlyAdded;
/// <summary> /// <summary>
/// Gets the Ombi settings. /// Gets the Ombi settings.
@ -607,7 +610,7 @@ namespace Ombi.Controllers
var model = Mapper.Map<EmailNotificationsViewModel>(emailSettings); var model = Mapper.Map<EmailNotificationsViewModel>(emailSettings);
// Lookup to see if we have any templates saved // Lookup to see if we have any templates saved
model.NotificationTemplates = await BuildTemplates(NotificationAgent.Email); model.NotificationTemplates = BuildTemplates(NotificationAgent.Email);
return model; return model;
} }
@ -654,7 +657,7 @@ namespace Ombi.Controllers
var model = Mapper.Map<DiscordNotificationsViewModel>(emailSettings); var model = Mapper.Map<DiscordNotificationsViewModel>(emailSettings);
// Lookup to see if we have any templates saved // Lookup to see if we have any templates saved
model.NotificationTemplates = await BuildTemplates(NotificationAgent.Discord); model.NotificationTemplates = BuildTemplates(NotificationAgent.Discord);
return model; return model;
} }
@ -689,7 +692,7 @@ namespace Ombi.Controllers
var model = Mapper.Map<TelegramNotificationsViewModel>(emailSettings); var model = Mapper.Map<TelegramNotificationsViewModel>(emailSettings);
// Lookup to see if we have any templates saved // Lookup to see if we have any templates saved
model.NotificationTemplates = await BuildTemplates(NotificationAgent.Telegram); model.NotificationTemplates = BuildTemplates(NotificationAgent.Telegram);
return model; return model;
} }
@ -723,7 +726,7 @@ namespace Ombi.Controllers
var model = Mapper.Map<PushbulletNotificationViewModel>(settings); var model = Mapper.Map<PushbulletNotificationViewModel>(settings);
// Lookup to see if we have any templates saved // Lookup to see if we have any templates saved
model.NotificationTemplates = await BuildTemplates(NotificationAgent.Pushbullet); model.NotificationTemplates = BuildTemplates(NotificationAgent.Pushbullet);
return model; return model;
} }
@ -757,7 +760,7 @@ namespace Ombi.Controllers
var model = Mapper.Map<PushoverNotificationViewModel>(settings); var model = Mapper.Map<PushoverNotificationViewModel>(settings);
// Lookup to see if we have any templates saved // Lookup to see if we have any templates saved
model.NotificationTemplates = await BuildTemplates(NotificationAgent.Pushover); model.NotificationTemplates = BuildTemplates(NotificationAgent.Pushover);
return model; return model;
} }
@ -792,7 +795,7 @@ namespace Ombi.Controllers
var model = Mapper.Map<SlackNotificationsViewModel>(settings); var model = Mapper.Map<SlackNotificationsViewModel>(settings);
// Lookup to see if we have any templates saved // Lookup to see if we have any templates saved
model.NotificationTemplates = await BuildTemplates(NotificationAgent.Slack); model.NotificationTemplates = BuildTemplates(NotificationAgent.Slack);
return model; return model;
} }
@ -826,7 +829,7 @@ namespace Ombi.Controllers
var model = Mapper.Map<MattermostNotificationsViewModel>(settings); var model = Mapper.Map<MattermostNotificationsViewModel>(settings);
// Lookup to see if we have any templates saved // Lookup to see if we have any templates saved
model.NotificationTemplates = await BuildTemplates(NotificationAgent.Mattermost); model.NotificationTemplates = BuildTemplates(NotificationAgent.Mattermost);
return model; return model;
} }
@ -860,17 +863,62 @@ namespace Ombi.Controllers
var model = Mapper.Map<MobileNotificationsViewModel>(settings); var model = Mapper.Map<MobileNotificationsViewModel>(settings);
// Lookup to see if we have any templates saved // Lookup to see if we have any templates saved
model.NotificationTemplates = await BuildTemplates(NotificationAgent.Mobile); model.NotificationTemplates = BuildTemplates(NotificationAgent.Mobile);
return model; return model;
} }
private async Task<List<NotificationTemplates>> BuildTemplates(NotificationAgent agent) /// <summary>
/// Saves the Newsletter notification settings.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
[HttpPost("notifications/newsletter")]
public async Task<bool> NewsletterSettings([FromBody] NewsletterNotificationViewModel model)
{ {
var templates = await TemplateRepository.GetAllTemplates(agent); // Save the email settings
return templates.OrderBy(x => x.NotificationType.ToString()).ToList(); var settings = Mapper.Map<NewsletterSettings>(model);
var result = await Save(settings);
// Save the templates
await TemplateRepository.Update(model.NotificationTemplate);
return result;
} }
[ApiExplorerSettings(IgnoreApi = true)]
[HttpPost("notifications/newsletterdatabase")]
public async Task<bool> UpdateNewsletterDatabase()
{
return await _recentlyAdded.UpdateRecentlyAddedDatabase();
}
/// <summary>
/// Gets the Newsletter Notification Settings.
/// </summary>
/// <returns></returns>
[HttpGet("notifications/newsletter")]
public async Task<NewsletterNotificationViewModel> NewsletterSettings()
{
var settings = await Get<NewsletterSettings>();
var model = Mapper.Map<NewsletterNotificationViewModel>(settings);
// Lookup to see if we have any templates saved
var templates = BuildTemplates(NotificationAgent.Email, true);
model.NotificationTemplate = templates.FirstOrDefault(x => x.NotificationType == NotificationType.Newsletter);
return model;
}
private List<NotificationTemplates> BuildTemplates(NotificationAgent agent, bool showNewsletter = false)
{
var templates = TemplateRepository.GetAllTemplates(agent);
if (!showNewsletter)
{
// Make sure we do not display the newsletter
templates = templates.Where(x => x.NotificationType != NotificationType.Newsletter);
}
return templates.OrderBy(x => x.NotificationType.ToString()).ToList();
}
private async Task<T> Get<T>() private async Task<T> Get<T>()
{ {

View file

@ -80,7 +80,7 @@ module.exports = (env: any) => {
}, },
plugins: [ plugins: [
new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", Hammer: "hammerjs/hammer" }), // Global identifiers new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", Hammer: "hammerjs/hammer" }), // Global identifiers
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/, path.join(__dirname, './client')), // Workaround for https://github.com/angular/angular/issues/20357 new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/, path.join(__dirname,"./client")), // Workaround for https://github.com/angular/angular/issues/20357
new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/11580 new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/11580
new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/14898 new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/14898
extractCSS, extractCSS,