Merge branch 'develop' into backgroundanimation

This commit is contained in:
Jamie 2018-03-23 08:52:08 +00:00 committed by GitHub
commit a3cf3dde46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 5112 additions and 487 deletions

View file

@ -2,6 +2,26 @@
## (unreleased) ## (unreleased)
### **New Features**
- Added the ability to refresh out backend metadata (#2078) [Jamie]
### **Fixes**
- Fixed #2074 and #2079. [Jamie]
- Small changes to the auto updater, let's see how this works. [Jamie]
## v3.0.3030 (2018-03-14)
### **New Features**
- Updated the .Net core dependancies #2072. [Jamie]
## v3.0.3020 (2018-03-13)
### **Fixes** ### **Fixes**
- Small memory improvements in the Plex Sync. [Jamie] - Small memory improvements in the Plex Sync. [Jamie]
@ -22,6 +42,8 @@
- Experimental, set the Webpack base root to the ombi base path if we have it. This should hopefully fix the reverse proxy issues. [Jamie] - Experimental, set the Webpack base root to the ombi base path if we have it. This should hopefully fix the reverse proxy issues. [Jamie]
- Fixed #2056. [tidusjar]
## v3.0.3000 (2018-03-09) ## v3.0.3000 (2018-03-09)

View file

@ -32,9 +32,9 @@ namespace Ombi.Api.FanartTv
} }
} }
public async Task<MovieResult> GetMovieImages(int theMovieDbId, string token) public async Task<MovieResult> GetMovieImages(string movieOrImdbId, string token)
{ {
var request = new Request($"movies/{theMovieDbId}", Endpoint, HttpMethod.Get); var request = new Request($"movies/{movieOrImdbId}", Endpoint, HttpMethod.Get);
request.AddHeader("api-key", token); request.AddHeader("api-key", token);
return await Api.Request<MovieResult>(request); return await Api.Request<MovieResult>(request);

View file

@ -5,7 +5,7 @@ namespace Ombi.Api.FanartTv
{ {
public interface IFanartTvApi public interface IFanartTvApi
{ {
Task<MovieResult> GetMovieImages(int theMovieDbId, string token); Task<MovieResult> GetMovieImages(string movieOrImdbId, string token);
Task<TvResult> GetTvImages(int tvdbId, string token); Task<TvResult> GetTvImages(int tvdbId, string token);
} }
} }

View file

@ -1,4 +1,7 @@
using System.IO; using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -6,6 +9,7 @@ using System.Xml.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Helpers; using Ombi.Helpers;
using Polly;
namespace Ombi.Api namespace Ombi.Api
{ {
@ -36,6 +40,30 @@ namespace Ombi.Api
if (!httpResponseMessage.IsSuccessStatusCode) if (!httpResponseMessage.IsSuccessStatusCode)
{ {
LogError(request, httpResponseMessage); LogError(request, httpResponseMessage);
if (request.Retry)
{
var result = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => request.StatusCodeToRetry.Contains(r.StatusCode))
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(10),
}, (exception, timeSpan, context) =>
{
Logger.LogError(LoggingEvents.Api,
$"Retrying RequestUri: {request.FullUri} Because we got Status Code: {exception?.Result?.StatusCode}");
});
httpResponseMessage = await result.ExecuteAsync(async () =>
{
using (var req = await httpRequestMessage.Clone())
{
return await _client.SendAsync(req);
}
});
}
} }
// do something with the response // do something with the response

View file

@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace Ombi.Api
{
public static class HttpRequestExtnesions
{
public static async Task<HttpRequestMessage> Clone(this HttpRequestMessage request)
{
var clone = new HttpRequestMessage(request.Method, request.RequestUri)
{
Content = await request.Content.Clone(),
Version = request.Version
};
foreach (KeyValuePair<string, object> prop in request.Properties)
{
clone.Properties.Add(prop);
}
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
{
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return clone;
}
public static async Task<HttpContent> Clone(this HttpContent content)
{
if (content == null) return null;
var ms = new MemoryStream();
await content.CopyToAsync(ms);
ms.Position = 0;
var clone = new StreamContent(ms);
foreach (KeyValuePair<string, IEnumerable<string>> header in content.Headers)
{
clone.Headers.Add(header.Key, header.Value);
}
return clone;
}
}
}

View file

@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="Polly" Version="5.8.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" /> <PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
</ItemGroup> </ItemGroup>

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
@ -25,6 +26,9 @@ namespace Ombi.Api
public string BaseUrl { get; } public string BaseUrl { get; }
public HttpMethod HttpMethod { get; } public HttpMethod HttpMethod { get; }
public bool Retry { get; set; }
public List<HttpStatusCode> StatusCodeToRetry { get; set; } = new List<HttpStatusCode>();
public Action<string> OnBeforeDeserialization { get; set; } public Action<string> OnBeforeDeserialization { get; set; }
private string FullUrl private string FullUrl

View file

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Core.Models;
namespace Ombi.Core.Engine
{
public interface IRecentlyAddedEngine
{
IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies();
IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to);
IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason);
IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(bool groupBySeason);
Task<bool> UpdateRecentlyAddedDatabase();
}
}

View file

@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models;
using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using RecentlyAddedType = Ombi.Store.Entities.RecentlyAddedType;
namespace Ombi.Core.Engine
{
public class RecentlyAddedEngine : IRecentlyAddedEngine
{
public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> recentlyAdded)
{
_plex = plex;
_emby = emby;
_recentlyAddedLog = recentlyAdded;
}
private readonly IPlexContentRepository _plex;
private readonly IEmbyContentRepository _emby;
private readonly IRepository<RecentlyAddedLog> _recentlyAddedLog;
public IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to)
{
var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie && x.AddedAt > from && x.AddedAt < to);
var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie && x.AddedAt > from && x.AddedAt < to);
return GetRecentlyAddedMovies(plexMovies, embyMovies).Take(30);
}
public IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies()
{
var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie);
var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie);
return GetRecentlyAddedMovies(plexMovies, embyMovies);
}
public IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason)
{
var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show && x.AddedAt > from && x.AddedAt < to);
var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series && x.AddedAt > from && x.AddedAt < to);
return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason).Take(30);
}
public IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(bool groupBySeason)
{
var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show);
var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series);
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,
ContentType = ContentType.Parent
});
}
else
{
// Add the episodes
foreach (var ep in p.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Plex,
ContentId = ep.Id,
ContentType = ContentType.Episode
});
}
}
}
foreach (var e in embyContent)
{
if (e.Type == EmbyMediaType.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentId = e.Id,
ContentType = ContentType.Parent
});
}
else
{
// Add the episodes
foreach (var ep in e.Episodes)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Emby,
ContentId = ep.Id,
ContentType = ContentType.Episode
});
}
}
}
await _recentlyAddedLog.AddRange(recentlyAddedLog);
return true;
}
private IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(IQueryable<PlexServerContent> plexTv, IQueryable<EmbyContent> embyTv,
bool groupBySeason)
{
var model = new HashSet<RecentlyAddedTvModel>();
TransformPlexShows(plexTv, model);
TransformEmbyShows(embyTv, model);
if (groupBySeason)
{
return model.DistinctBy(x => x.SeasonNumber);
}
return model;
}
private IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(IQueryable<PlexServerContent> plexMovies, IQueryable<EmbyContent> embyMovies)
{
var model = new HashSet<RecentlyAddedMovieModel>();
TransformPlexMovies(plexMovies, model);
TransformEmbyMovies(embyMovies, model);
return model;
}
private static void TransformEmbyMovies(IQueryable<EmbyContent> embyMovies, HashSet<RecentlyAddedMovieModel> model)
{
foreach (var emby in embyMovies)
{
model.Add(new RecentlyAddedMovieModel
{
Id = emby.Id,
ImdbId = emby.ProviderId,
AddedAt = emby.AddedAt,
Title = emby.Title,
});
}
}
private static void TransformPlexMovies(IQueryable<PlexServerContent> plexMovies, HashSet<RecentlyAddedMovieModel> model)
{
foreach (var plex in plexMovies)
{
model.Add(new RecentlyAddedMovieModel
{
Id = plex.Id,
ImdbId = plex.ImdbId,
TheMovieDbId = plex.TheMovieDbId,
AddedAt = plex.AddedAt,
Title = plex.Title,
Quality = plex.Quality,
ReleaseYear = plex.ReleaseYear
});
}
}
private static void TransformPlexShows(IQueryable<PlexServerContent> plexShows, HashSet<RecentlyAddedTvModel> model)
{
foreach (var plex in plexShows)
{
foreach (var season in plex.Seasons)
{
foreach (var episode in plex.Episodes)
{
model.Add(new RecentlyAddedTvModel
{
Id = plex.Id,
ImdbId = plex.ImdbId,
TheMovieDbId = plex.TheMovieDbId,
AddedAt = plex.AddedAt,
Title = plex.Title,
Quality = plex.Quality,
ReleaseYear = plex.ReleaseYear,
TvDbId = plex.TvDbId,
EpisodeNumber = episode.EpisodeNumber,
SeasonNumber = season.SeasonNumber
});
}
}
}
}
private static void TransformEmbyShows(IQueryable<EmbyContent> embyShows, HashSet<RecentlyAddedTvModel> model)
{
foreach (var emby in embyShows)
{
foreach (var episode in emby.Episodes)
{
model.Add(new RecentlyAddedTvModel
{
Id = emby.Id,
ImdbId = emby.ProviderId,
AddedAt = emby.AddedAt,
Title = emby.Title,
EpisodeNumber = episode.EpisodeNumber,
SeasonNumber = episode.SeasonNumber
});
}
}
}
}
}

View file

@ -0,0 +1,23 @@
using System;
namespace Ombi.Core.Models
{
public class RecentlyAddedMovieModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Overview { get; set; }
public string ImdbId { get; set; }
public string TvDbId { get; set; }
public string TheMovieDbId { get; set; }
public string ReleaseYear { get; set; }
public DateTime AddedAt { get; set; }
public string Quality { get; set; }
}
public enum RecentlyAddedType
{
Plex,
Emby
}
}

View file

@ -0,0 +1,19 @@
using System;
namespace Ombi.Core.Models
{
public class RecentlyAddedTvModel
{
public int Id { get; set; }
public string Title { get; set; } // Series Title
public string Overview { get; set; }
public string ImdbId { get; set; }
public string TvDbId { get; set; }
public string TheMovieDbId { get; set; }
public string ReleaseYear { get; set; }
public DateTime AddedAt { get; set; }
public string Quality { get; set; }
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
}
}

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

@ -79,6 +79,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ITvSearchEngine, TvSearchEngine>(); services.AddTransient<ITvSearchEngine, TvSearchEngine>();
services.AddTransient<IRuleEvaluator, RuleEvaluator>(); services.AddTransient<IRuleEvaluator, RuleEvaluator>();
services.AddTransient<IMovieSender, MovieSender>(); services.AddTransient<IMovieSender, MovieSender>();
services.AddTransient<IRecentlyAddedEngine, RecentlyAddedEngine>();
services.AddTransient<ITvSender, TvSender>(); services.AddTransient<ITvSender, TvSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>(); services.AddTransient<IMassEmailSender, MassEmailSender>();
} }
@ -172,6 +173,8 @@ namespace Ombi.DependencyInjection
services.AddTransient<ICouchPotatoSync, CouchPotatoSync>(); services.AddTransient<ICouchPotatoSync, CouchPotatoSync>();
services.AddTransient<IProcessProvider, ProcessProvider>(); services.AddTransient<IProcessProvider, ProcessProvider>();
services.AddTransient<ISickRageSync, SickRageSync>(); services.AddTransient<ISickRageSync, SickRageSync>();
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, OmbiUser username)
{
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = username.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,46 +17,53 @@ 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) ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
INewsletterJob newsletter)
{ {
PlexContentSync = plexContentSync; _plexContentSync = plexContentSync;
RadarrSync = radarrSync; _radarrSync = radarrSync;
Updater = updater; _updater = updater;
EmbyContentSync = embySync; _embyContentSync = embySync;
PlexUserImporter = userImporter; _plexUserImporter = userImporter;
EmbyUserImporter = embyUserImporter; _embyUserImporter = embyUserImporter;
SonarrSync = cache; _sonarrSync = cache;
CpCache = cpCache; _cpCache = cpCache;
JobSettings = jobsettings; _jobSettings = jobsettings;
SrSync = srSync; _srSync = srSync;
_refreshMetadata = refresh;
_newsletter = newsletter;
} }
private IPlexContentSync PlexContentSync { get; } private readonly IPlexContentSync _plexContentSync;
private IRadarrSync RadarrSync { get; } private readonly IRadarrSync _radarrSync;
private IOmbiAutomaticUpdater Updater { get; } private readonly IOmbiAutomaticUpdater _updater;
private IPlexUserImporter PlexUserImporter { get; } private readonly IPlexUserImporter _plexUserImporter;
private IEmbyContentSync EmbyContentSync { get; } private readonly IEmbyContentSync _embyContentSync;
private IEmbyUserImporter EmbyUserImporter { get; } private readonly IEmbyUserImporter _embyUserImporter;
private ISonarrSync SonarrSync { get; } private readonly ISonarrSync _sonarrSync;
private ICouchPotatoSync CpCache { get; } private readonly ICouchPotatoSync _cpCache;
private ISickRageSync SrSync { get; } private readonly ISickRageSync _srSync;
private ISettingsService<JobSettings> JobSettings { get; set; } private readonly ISettingsService<JobSettings> _jobSettings;
private readonly IRefreshMetadata _refreshMetadata;
private readonly INewsletterJob _newsletter;
public void Setup() public void Setup()
{ {
var s = JobSettings.GetSettings(); var s = _jobSettings.GetSettings();
RecurringJob.AddOrUpdate(() => EmbyContentSync.Start(), JobSettingsHelper.EmbyContent(s)); RecurringJob.AddOrUpdate(() => _embyContentSync.Start(), JobSettingsHelper.EmbyContent(s));
RecurringJob.AddOrUpdate(() => SonarrSync.Start(), JobSettingsHelper.Sonarr(s)); RecurringJob.AddOrUpdate(() => _sonarrSync.Start(), JobSettingsHelper.Sonarr(s));
RecurringJob.AddOrUpdate(() => RadarrSync.CacheContent(), JobSettingsHelper.Radarr(s)); RecurringJob.AddOrUpdate(() => _radarrSync.CacheContent(), JobSettingsHelper.Radarr(s));
RecurringJob.AddOrUpdate(() => PlexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s)); RecurringJob.AddOrUpdate(() => _plexContentSync.CacheContent(), JobSettingsHelper.PlexContent(s));
RecurringJob.AddOrUpdate(() => CpCache.Start(), JobSettingsHelper.CouchPotato(s)); RecurringJob.AddOrUpdate(() => _cpCache.Start(), JobSettingsHelper.CouchPotato(s));
RecurringJob.AddOrUpdate(() => SrSync.Start(), JobSettingsHelper.SickRageSync(s)); RecurringJob.AddOrUpdate(() => _srSync.Start(), JobSettingsHelper.SickRageSync(s));
RecurringJob.AddOrUpdate(() => _refreshMetadata.Start(), JobSettingsHelper.RefreshMetadata(s));
RecurringJob.AddOrUpdate(() => Updater.Update(null), JobSettingsHelper.Updater(s)); RecurringJob.AddOrUpdate(() => _updater.Update(null), JobSettingsHelper.Updater(s));
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,11 @@
using System.Threading.Tasks;
using Ombi.Settings.Settings.Models.Notifications;
namespace Ombi.Schedule.Jobs.Ombi
{
public interface INewsletterJob : IBaseJob
{
Task Start();
Task Start(NewsletterSettings settings, bool test);
}
}

View file

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

View file

@ -0,0 +1,605 @@
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;
_emailSettings.ClearCache();
_customizationSettings.ClearCache();
_newsletterSettings.ClearCache();
}
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(NewsletterSettings settings, bool test)
{
if (!settings.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 => x.Type == PlexMediaTypeEntity.Movie && !addedPlexMovieLogIds.Contains(x.Id));
var embyContentMoviesToSend = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !addedEmbyMoviesLogIds.Contains(x.Id));
var plexContentTvToSend = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Show && x.Episodes.Any(e => !addedPlexEpisodesLogIds.Contains(e.Id)));
var embyContentTvToSend = embyContent.Where(x => x.Type == EmbyMediaType.Series && x.Episodes.Any(e => !addedEmbyEpisodesLogIds.Contains(e.Id)));
var plexContentToSend = plexContentMoviesToSend.Union(plexContentTvToSend);
var embyContentToSend = embyContentMoviesToSend.Union(embyContentTvToSend);
var body = string.Empty;
if (test)
{
var plexm = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var plext = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Show).OrderByDescending(x => x.AddedAt).Take(10);
var embyt = embyContent.Where(x => x.Type == EmbyMediaType.Series).OrderByDescending(x => x.AddedAt).Take(10);
body = await BuildHtml(plexm.Union(plext), embym.Union(embyt));
}
else
{
body = await BuildHtml(plexContentToSend, embyContentToSend);
if (body.IsNullOrEmpty())
{
return;
}
}
if (!test)
{
// 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 messageContent = ParseTemplate(template, customization, user);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
emailTasks.Add(_email.Send(
new NotificationMessage {Message = html, Subject = messageContent.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());
}
else
{
var admins = await _userManager.GetUsersInRoleAsync(OmbiRoles.Admin);
foreach (var a in admins)
{
var messageContent = ParseTemplate(template, customization, a);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
await _email.Send(
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email },
emailSettings);
}
}
}
public async Task Start()
{
var newsletterSettings = await _newsletterSettings.GetSettingsAsync();
await Start(newsletterSettings, false);
}
private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings, OmbiUser username)
{
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
curlys.SetupNewsletter(settings, username);
return resolver.ParseMessage(template, curlys);
}
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

@ -0,0 +1,244 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.TvMaze;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
namespace Ombi.Schedule.Jobs.Ombi
{
public class RefreshMetadata : IRefreshMetadata
{
public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo,
ILogger<RefreshMetadata> log, ITvMazeApi tvApi, ISettingsService<PlexSettings> plexSettings,
IMovieDbApi movieApi)
{
_plexRepo = plexRepo;
_embyRepo = embyRepo;
_log = log;
_movieApi = movieApi;
_tvApi = tvApi;
_plexSettings = plexSettings;
}
private readonly IPlexContentRepository _plexRepo;
private readonly IEmbyContentRepository _embyRepo;
private readonly ILogger _log;
private readonly IMovieDbApi _movieApi;
private readonly ITvMazeApi _tvApi;
private readonly ISettingsService<PlexSettings> _plexSettings;
public async Task Start()
{
_log.LogInformation("Starting the Metadata refresh");
try
{
var settings = await _plexSettings.GetSettingsAsync();
if (settings.Enable)
{
await StartPlex();
}
}
catch (Exception e)
{
_log.LogError(e, "Exception when refreshing the Plex Metadata");
throw;
}
}
private async Task StartPlex()
{
await StartPlexMovies();
// Now Tv
await StartPlexTv();
}
private async Task StartPlexTv()
{
var allTv = _plexRepo.GetAll().Where(x =>
x.Type == PlexMediaTypeEntity.Show && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue() || !x.TvDbId.HasValue()));
var tvCount = 0;
foreach (var show in allTv)
{
var hasImdb = show.ImdbId.HasValue();
var hasTheMovieDb = show.TheMovieDbId.HasValue();
var hasTvDbId = show.TvDbId.HasValue();
if (!hasTheMovieDb)
{
var id = await GetTheMovieDbId(hasTvDbId, hasImdb, show.TvDbId, show.ImdbId, show.Title);
show.TheMovieDbId = id;
}
if (!hasImdb)
{
var id = await GetImdbId(hasTheMovieDb, hasTvDbId, show.Title, show.TheMovieDbId, show.TvDbId);
show.ImdbId = id;
_plexRepo.UpdateWithoutSave(show);
}
if (!hasTvDbId)
{
var id = await GetTvDbId(hasTheMovieDb, hasImdb, show.TheMovieDbId, show.ImdbId, show.Title);
show.TvDbId = id;
_plexRepo.UpdateWithoutSave(show);
}
tvCount++;
if (tvCount >= 20)
{
await _plexRepo.SaveChangesAsync();
tvCount = 0;
}
}
await _plexRepo.SaveChangesAsync();
}
private async Task StartPlexMovies()
{
var allMovies = _plexRepo.GetAll().Where(x =>
x.Type == PlexMediaTypeEntity.Movie && (!x.TheMovieDbId.HasValue() || !x.ImdbId.HasValue()));
int movieCount = 0;
foreach (var movie in allMovies)
{
var hasImdb = movie.ImdbId.HasValue();
var hasTheMovieDb = movie.TheMovieDbId.HasValue();
// Movies don't really use TheTvDb
if (!hasImdb)
{
var imdbId = await GetImdbId(hasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty);
movie.ImdbId = imdbId;
_plexRepo.UpdateWithoutSave(movie);
}
if (!hasTheMovieDb)
{
var id = await GetTheMovieDbId(false, hasImdb, string.Empty, movie.ImdbId, movie.Title);
movie.TheMovieDbId = id;
_plexRepo.UpdateWithoutSave(movie);
}
movieCount++;
if (movieCount >= 20)
{
await _plexRepo.SaveChangesAsync();
movieCount = 0;
}
}
await _plexRepo.SaveChangesAsync();
}
private async Task<string> GetTheMovieDbId(bool hasTvDbId, bool hasImdb, string tvdbID, string imdbId, string title)
{
_log.LogInformation("The Media item {0} does not have a TheMovieDbId, searching for TheMovieDbId", title);
FindResult result = null;
var hasResult = false;
if (hasTvDbId)
{
result = await _movieApi.Find(tvdbID, ExternalSource.tvdb_id);
hasResult = result?.tv_results?.Length > 0;
_log.LogInformation("Setting Show {0} because we have TvDbId, result: {1}", title, hasResult);
}
if (hasImdb && !hasResult)
{
result = await _movieApi.Find(imdbId, ExternalSource.imdb_id);
hasResult = result?.tv_results?.Length > 0;
_log.LogInformation("Setting Show {0} because we have ImdbId, result: {1}", title, hasResult);
}
if (hasResult)
{
return result.tv_results?[0]?.id.ToString() ?? string.Empty;
}
return string.Empty;
}
private async Task<string> GetImdbId(bool hasTheMovieDb, bool hasTvDbId, string title, string theMovieDbId, string tvDbId)
{
_log.LogInformation("The media item {0} does not have a ImdbId, searching for ImdbId", title);
// Looks like TV Maze does not provide the moviedb id, neither does the TV endpoint on TheMovieDb
if (hasTheMovieDb)
{
_log.LogInformation("The show {0} has TheMovieDbId but not ImdbId, searching for ImdbId", title);
if (int.TryParse(theMovieDbId, out var id))
{
var result = await _movieApi.GetTvExternals(id);
return result.imdb_id;
}
}
if (hasTvDbId)
{
_log.LogInformation("The show {0} has tvdbid but not ImdbId, searching for ImdbId", title);
if (int.TryParse(tvDbId, out var id))
{
var result = await _tvApi.ShowLookupByTheTvDbId(id);
return result?.externals?.imdb;
}
}
return string.Empty;
}
private async Task<string> GetTvDbId(bool hasTheMovieDb, bool hasImdb, string theMovieDbId, string imdbId, string title)
{
_log.LogInformation("The media item {0} does not have a TvDbId, searching for TvDbId", title);
if (hasTheMovieDb)
{
_log.LogInformation("The show {0} has theMovieDBId but not ImdbId, searching for ImdbId", title);
if (int.TryParse(theMovieDbId, out var id))
{
var result = await _movieApi.GetTvExternals(id);
return result.tvdb_id.ToString();
}
}
if (hasImdb)
{
_log.LogInformation("The show {0} has ImdbId but not ImdbId, searching for ImdbId", title);
var result = await _movieApi.Find(imdbId, ExternalSource.imdb_id);
if (result?.tv_results?.Length > 0)
{
var movieId = result.tv_results?[0]?.id ?? 0;
var externalResult = await _movieApi.GetTvExternals(movieId);
return externalResult.imdb_id;
}
}
return string.Empty;
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_plexRepo?.Dispose();
_embyRepo?.Dispose();
_plexSettings?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View file

@ -32,8 +32,10 @@
<ProjectReference Include="..\Ombi.Api.Service\Ombi.Api.Service.csproj" /> <ProjectReference Include="..\Ombi.Api.Service\Ombi.Api.Service.csproj" />
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" /> <ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />
<ProjectReference Include="..\Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj" /> <ProjectReference Include="..\Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj" />
<ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" /> <ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" /> <ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" />
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -46,7 +46,8 @@ namespace Ombi.Schedule.Processor
if (masterBranch) if (masterBranch)
{ {
latestRelease = doc.DocumentNode.Descendants("h2") latestRelease = doc.DocumentNode.Descendants("h2")
.FirstOrDefault(x => x.InnerText != "(unreleased)"); .FirstOrDefault(x => x.InnerText == "(unreleased)");
// TODO: Change this to InnterText != "(unreleased)" once we go live and it's not a prerelease
} }
else else
{ {
@ -78,9 +79,9 @@ namespace Ombi.Schedule.Processor
Downloads = new List<Downloads>() Downloads = new List<Downloads>()
}; };
var releaseTag = latestRelease.InnerText.Substring(0, 6);
if (masterBranch) if (masterBranch)
{ {
var releaseTag = latestRelease.InnerText.Substring(0, 9);
await GetGitubRelease(release, releaseTag); await GetGitubRelease(release, releaseTag);
} }
else else
@ -147,7 +148,7 @@ namespace Ombi.Schedule.Processor
var builds = await _api.Request<AppveyorBranchResult>(request); var builds = await _api.Request<AppveyorBranchResult>(request);
var jobId = builds.build.jobs.FirstOrDefault()?.jobId ?? string.Empty; var jobId = builds.build.jobs.FirstOrDefault()?.jobId ?? string.Empty;
if (builds.build.finished == DateTime.MinValue) if (builds.build.finished == DateTime.MinValue || builds.build.status.Equals("failed"))
{ {
return; return;
} }

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

@ -10,5 +10,7 @@
public string AutomaticUpdater { get; set; } public string AutomaticUpdater { get; set; }
public string UserImporter { get; set; } public string UserImporter { get; set; }
public string SickRageSync { get; set; } public string SickRageSync { get; set; }
public string RefreshMetadata { get; set; }
public string Newsletter { get; set; }
} }
} }

View file

@ -1,4 +1,5 @@
using Ombi.Helpers; using System;
using Ombi.Helpers;
namespace Ombi.Settings.Settings.Models namespace Ombi.Settings.Settings.Models
{ {
@ -35,10 +36,18 @@ namespace Ombi.Settings.Settings.Models
{ {
return Get(s.UserImporter, Cron.Daily()); return Get(s.UserImporter, Cron.Daily());
} }
public static string Newsletter(JobSettings s)
{
return Get(s.Newsletter, Cron.Weekly(DayOfWeek.Friday, 12));
}
public static string SickRageSync(JobSettings s) public static string SickRageSync(JobSettings s)
{ {
return Get(s.SickRageSync, Cron.Hourly(35)); return Get(s.SickRageSync, Cron.Hourly(35));
} }
public static string RefreshMetadata(JobSettings s)
{
return Get(s.RefreshMetadata, Cron.Daily(3));
}
private static string Get(string settings, string defaultCron) private static string Get(string settings, string defaultCron)

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

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Helpers; using Ombi.Helpers;
@ -37,6 +38,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; }
@ -113,6 +115,12 @@ namespace Ombi.Store.Context
// VACUUM; // VACUUM;
Database.ExecuteSqlCommand("VACUUM;"); Database.ExecuteSqlCommand("VACUUM;");
// Make sure we have the roles
var roles = Roles.Where(x => x.Name == OmbiRoles.RecievesNewsletter);
if (!roles.Any())
{
Roles.Add(new IdentityRole(OmbiRoles.RecievesNewsletter));
}
//Check if templates exist //Check if templates exist
var templates = NotificationTemplates.ToList(); var templates = NotificationTemplates.ToList();
@ -218,6 +226,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,936 @@
// <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("20180322204610_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>("ContentType");
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,34 @@
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),
ContentType = 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,24 @@ 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>("ContentType");
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

@ -45,9 +45,9 @@ namespace Ombi.Store.Repository
private IOmbiContext Db { get; } private IOmbiContext Db { get; }
public async Task<IEnumerable<EmbyContent>> GetAll() public IQueryable<EmbyContent> GetAll()
{ {
return await Db.EmbyContent.ToListAsync(); return Db.EmbyContent.AsQueryable();
} }
public async Task AddRange(IEnumerable<EmbyContent> content) public async Task AddRange(IEnumerable<EmbyContent> content)

View file

@ -13,7 +13,7 @@ namespace Ombi.Store.Repository
Task<bool> ContentExists(string providerId); Task<bool> ContentExists(string providerId);
IQueryable<EmbyContent> Get(); IQueryable<EmbyContent> Get();
Task<EmbyContent> Get(string providerId); Task<EmbyContent> Get(string providerId);
Task<IEnumerable<EmbyContent>> GetAll(); IQueryable<EmbyContent> GetAll();
Task<EmbyContent> GetByEmbyId(string embyId); Task<EmbyContent> GetByEmbyId(string embyId);
Task Update(EmbyContent existingContent); Task Update(EmbyContent existingContent);
IQueryable<EmbyEpisode> GetAllEpisodes(); IQueryable<EmbyEpisode> GetAllEpisodes();

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

@ -22,5 +22,7 @@ namespace Ombi.Store.Repository
Task DeleteEpisode(PlexEpisode content); Task DeleteEpisode(PlexEpisode content);
void DeleteWithoutSave(PlexServerContent content); void DeleteWithoutSave(PlexServerContent content);
void DeleteWithoutSave(PlexEpisode content); void DeleteWithoutSave(PlexEpisode content);
Task UpdateRange(IEnumerable<PlexServerContent> existingContent);
void UpdateWithoutSave(PlexServerContent existingContent);
} }
} }

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
using Ombi.Store.Entities; using Ombi.Store.Entities;
@ -24,5 +25,6 @@ namespace Ombi.Store.Repository
where TEntity : class; where TEntity : class;
Task ExecuteSql(string sql); Task ExecuteSql(string sql);
DbSet<T> _db { get; }
} }
} }

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)
@ -40,6 +40,11 @@ namespace Ombi.Store.Repository
public async Task Update(NotificationTemplates template) public async Task Update(NotificationTemplates template)
{ {
if (Db.Entry(template).State == EntityState.Detached)
{
Db.Attach(template);
Db.Entry(template).State = EntityState.Modified;
}
await Db.SaveChangesAsync(); await Db.SaveChangesAsync();
} }
@ -60,5 +65,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

@ -97,6 +97,16 @@ namespace Ombi.Store.Repository
Db.PlexServerContent.Update(existingContent); Db.PlexServerContent.Update(existingContent);
await Db.SaveChangesAsync(); await Db.SaveChangesAsync();
} }
public void UpdateWithoutSave(PlexServerContent existingContent)
{
Db.PlexServerContent.Update(existingContent);
}
public async Task UpdateRange(IEnumerable<PlexServerContent> existingContent)
{
Db.PlexServerContent.UpdateRange(existingContent);
await Db.SaveChangesAsync();
}
public IQueryable<PlexEpisode> GetAllEpisodes() public IQueryable<PlexEpisode> GetAllEpisodes()
{ {

View file

@ -17,7 +17,7 @@ namespace Ombi.Store.Repository
_ctx = ctx; _ctx = ctx;
_db = _ctx.Set<T>(); _db = _ctx.Set<T>();
} }
private readonly DbSet<T> _db; public DbSet<T> _db { get; }
private readonly IOmbiContext _ctx; private readonly IOmbiContext _ctx;
public async Task<T> Find(object key) public async Task<T> Find(object key)

View file

@ -15,5 +15,7 @@ namespace Ombi.Api.TheMovieDb
Task<List<MovieSearchResult>> TopRated(); Task<List<MovieSearchResult>> TopRated();
Task<List<MovieSearchResult>> Upcoming(); Task<List<MovieSearchResult>> Upcoming();
Task<List<MovieSearchResult>> SimilarMovies(int movieId); Task<List<MovieSearchResult>> SimilarMovies(int movieId);
Task<FindResult> Find(string externalId, ExternalSource source);
Task<TvExternals> GetTvExternals(int theMovieDbId);
} }
} }

View file

@ -0,0 +1,52 @@
namespace Ombi.Api.TheMovieDb.Models
{
public class FindResult
{
public Movie_Results[] movie_results { get; set; }
public object[] person_results { get; set; }
public TvResults[] tv_results { get; set; }
public object[] tv_episode_results { get; set; }
public object[] tv_season_results { get; set; }
}
public class Movie_Results
{
public bool adult { get; set; }
public string backdrop_path { get; set; }
public int[] genre_ids { get; set; }
public int id { get; set; }
public string original_language { get; set; }
public string original_title { get; set; }
public string overview { get; set; }
public string poster_path { get; set; }
public string release_date { get; set; }
public string title { get; set; }
public bool video { get; set; }
public float vote_average { get; set; }
public int vote_count { get; set; }
}
public class TvResults
{
public string original_name { get; set; }
public int id { get; set; }
public string name { get; set; }
public int vote_count { get; set; }
public float vote_average { get; set; }
public string first_air_date { get; set; }
public string poster_path { get; set; }
public int[] genre_ids { get; set; }
public string original_language { get; set; }
public string backdrop_path { get; set; }
public string overview { get; set; }
public string[] origin_country { get; set; }
}
public enum ExternalSource
{
imdb_id,
tvdb_id
}
}

View file

@ -0,0 +1,16 @@
namespace Ombi.Api.TheMovieDb.Models
{
public class TvExternals
{
public string imdb_id { get; set; }
public string freebase_mid { get; set; }
public string freebase_id { get; set; }
public int tvdb_id { get; set; }
public int tvrage_id { get; set; }
public string facebook_id { get; set; }
public object instagram_id { get; set; }
public object twitter_id { get; set; }
public int id { get; set; }
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoMapper; using AutoMapper;
@ -25,15 +26,37 @@ namespace Ombi.Api.TheMovieDb
{ {
var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<MovieResponse>(request); var result = await Api.Request<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(result); return Mapper.Map<MovieResponseDto>(result);
} }
public async Task<FindResult> Find(string externalId, ExternalSource source)
{
var request = new Request($"find/{externalId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
request.AddQueryString("external_source", source.ToString());
return await Api.Request<FindResult>(request);
}
public async Task<TvExternals> GetTvExternals(int theMovieDbId)
{
var request = new Request($"/tv/{theMovieDbId}/external_ids", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
return await Api.Request<TvExternals>(request);
}
public async Task<List<MovieSearchResult>> SimilarMovies(int movieId) public async Task<List<MovieSearchResult>> SimilarMovies(int movieId)
{ {
var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get); var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
@ -44,6 +67,7 @@ namespace Ombi.Api.TheMovieDb
var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates"); request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates");
AddRetry(request);
var result = await Api.Request<MovieResponse>(request); var result = await Api.Request<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(result); return Mapper.Map<MovieResponseDto>(result);
} }
@ -53,6 +77,7 @@ namespace Ombi.Api.TheMovieDb
var request = new Request($"search/movie", BaseUri, HttpMethod.Get); var request = new Request($"search/movie", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm); request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
@ -62,6 +87,7 @@ namespace Ombi.Api.TheMovieDb
{ {
var request = new Request($"movie/popular", BaseUri, HttpMethod.Get); var request = new Request($"movie/popular", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
} }
@ -70,6 +96,7 @@ namespace Ombi.Api.TheMovieDb
{ {
var request = new Request($"movie/top_rated", BaseUri, HttpMethod.Get); var request = new Request($"movie/top_rated", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
} }
@ -78,6 +105,7 @@ namespace Ombi.Api.TheMovieDb
{ {
var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get); var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
} }
@ -86,9 +114,14 @@ namespace Ombi.Api.TheMovieDb
{ {
var request = new Request($"movie/now_playing", BaseUri, HttpMethod.Get); var request = new Request($"movie/now_playing", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
} }
private static void AddRetry(Request request)
{
request.Retry = true;
request.StatusCodeToRetry.Add((HttpStatusCode)429);
}
} }
} }

View file

@ -34,6 +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 *ngIf="customizationSettings.recentlyAddedPage" class="nav navbar-nav">
<li id="RecentlyAdded" [routerLinkActive]="['active']">
<a [routerLink]="['/recentlyadded']">
<i class="fa fa-check"></i> {{ 'NavigationBar.RecentlyAdded' | translate }}</a>
</li>
</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']">
@ -46,6 +54,7 @@
<i class="fa fa-user"></i> {{ 'NavigationBar.UserManagement' | translate }}</a> <i class="fa fa-user"></i> {{ 'NavigationBar.UserManagement' | translate }}</a>
</li> </li>
</ul> </ul>
<ul *ngIf="hasRole('Admin') || hasRole('PowerUser')" class="nav navbar-nav"> <ul *ngIf="hasRole('Admin') || hasRole('PowerUser')" class="nav navbar-nav">
<li> <li>
<a href="https://www.paypal.me/PlexRequestsNet" target="_blank" pTooltip="{{ 'NavigationBar.DonateTooltip' | translate }}"> <a href="https://www.paypal.me/PlexRequestsNet" target="_blank" pTooltip="{{ 'NavigationBar.DonateTooltip' | translate }}">
@ -84,7 +93,7 @@
<a [routerLink]="['/usermanagement/updatedetails']"> <a [routerLink]="['/usermanagement/updatedetails']">
<i class="fa fa-key"></i>{{ 'NavigationBar.UpdateDetails' | translate }}</a> <i class="fa fa-key"></i>{{ 'NavigationBar.UpdateDetails' | translate }}</a>
</li> </li>
<li *ngIf="customizationSettings.mobile" [routerLinkActive]="['active']"> <li *ngIf="customizationSettings?.mobile" [routerLinkActive]="['active']">
<a href="#" (click)="openMobileApp($event)"> <a href="#" (click)="openMobileApp($event)">
<i class="fa fa-mobile"></i>{{ 'NavigationBar.OpenMobileApp' | translate }}</a> <i class="fa fa-mobile"></i>{{ 'NavigationBar.OpenMobileApp' | translate }}</a>
</li> </li>

View file

@ -52,6 +52,7 @@ const routes: Routes = [
{ loadChildren: "./usermanagement/usermanagement.module#UserManagementModule", path: "usermanagement" }, { loadChildren: "./usermanagement/usermanagement.module#UserManagementModule", path: "usermanagement" },
{ loadChildren: "./requests/requests.module#RequestsModule", path: "requests" }, { loadChildren: "./requests/requests.module#RequestsModule", path: "requests" },
{ loadChildren: "./search/search.module#SearchModule", path: "search" }, { loadChildren: "./search/search.module#SearchModule", path: "search" },
{ loadChildren: "./recentlyAdded/recentlyAdded.module#RecentlyAddedModule", path: "recentlyadded" },
]; ];
// AoT requires an exported function for factories // AoT requires an exported function for factories

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

@ -0,0 +1,29 @@
export interface IRecentlyAddedMovies {
id: number;
title: string;
overview: string;
imdbId: string;
theMovieDbId: string;
releaseYear: string;
addedAt: Date;
quality: string;
// For UI only
posterPath: string;
}
export interface IRecentlyAddedTvShows extends IRecentlyAddedMovies {
seasonNumber: number;
episodeNumber: number;
tvDbId: number;
}
export interface IRecentlyAddedRangeModel {
from: Date;
to: Date;
}
export enum RecentlyAddedType {
Plex,
Emby,
}

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 {
@ -125,6 +126,8 @@ export interface IJobSettings {
automaticUpdater: string; automaticUpdater: string;
userImporter: string; userImporter: string;
sickRageSync: string; sickRageSync: string;
refreshMetadata: string;
newsletter: string;
} }
export interface IIssueSettings extends ISettings { export interface IIssueSettings extends ISettings {
@ -193,3 +196,18 @@ export interface IDogNzbSettings extends ISettings {
export interface IIssueCategory extends ISettings { export interface IIssueCategory extends ISettings {
value: string; value: string;
} }
export interface ICronTestModel {
success: boolean;
message: string;
schedule: Date[];
}
export interface ICronViewModelBody {
expression: string;
}
export interface IJobSettingsViewModel {
result: boolean;
message: string;
}

View file

@ -13,3 +13,4 @@ export * from "./ISettings";
export * from "./ISonarr"; export * from "./ISonarr";
export * from "./IUser"; export * from "./IUser";
export * from "./IIssues"; export * from "./IIssues";
export * from "./IRecentlyAdded";

View file

@ -0,0 +1,51 @@
<h1>Recently Added</h1>
<input type="checkbox" [(ngModel)]="groupTv" (click)="change()" />
<hr />
<p-calendar [(ngModel)]="range" showButtonBar="true" selectionMode="range" (onClose)="close()"></p-calendar>
<hr />
<style>
.img-conatiner {
position: relative;
text-align: center;
color: white;
}
/* Bottom left text */
.bottom-left {
position: absolute;
bottom: 8px;
left: 16px;
}
</style>
<ngu-carousel [inputs]="carouselTile">
<ngu-tile NguCarouselItem *ngFor="let movie of movies">
<div class="img-container">
<img class="img-responsive poster" src="{{movie.posterPath}}" style="width: 300px" alt="poster">
<div class="bottom-left"> {{movie.title}}</div>
</div>
</ngu-tile>
<button NguCarouselPrev class='leftRs'><i class="fa fa-arrow-left"></i></button>
<button NguCarouselNext class='rightRs'><i class="fa fa-arrow-right"></i></button>
</ngu-carousel>
<hr/>
<ngu-carousel [inputs]="carouselTile">
<ngu-tile NguCarouselItem *ngFor="let t of tv">
<img class="img-responsive poster" src="{{t.posterPath}}" style="width: 300px" alt="poster">
<b>{{t.title}}</b>
<br>
<b>Season: {{t.seasonNumber}}</b>
<br>
<b>Episode: {{t.episodeNumber}}</b>
</ngu-tile>
<button NguCarouselPrev class='leftRs'><i class="fa fa-arrow-left"></i></button>
<button NguCarouselNext class='rightRs'><i class="fa fa-arrow-right"></i></button>
</ngu-carousel>

View file

@ -0,0 +1,127 @@
import { Component, OnInit } from "@angular/core";
import { NguCarousel } from "@ngu/carousel";
import { ImageService, RecentlyAddedService } from "../services";
import { IRecentlyAddedMovies, IRecentlyAddedTvShows } from "./../interfaces";
@Component({
templateUrl: "recentlyAdded.component.html",
styles: [`
.leftRs {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
width: 50px;
height: 50px;
box-shadow: 1px 2px 10px -1px rgba(0, 0, 0, .3);
border-radius: 100%;
left: 0;
background: #df691a;
}
.rightRs {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
width: 50px;
height: 50px;
box-shadow: 1px 2px 10px -1px rgba(0, 0, 0, .3);
border-radius: 100%;
right: 0;
background: #df691a;
}
`],
})
export class RecentlyAddedComponent implements OnInit {
public movies: IRecentlyAddedMovies[];
public tv: IRecentlyAddedTvShows[];
public range: Date[];
public groupTv: boolean = false;
// https://github.com/sheikalthaf/ngu-carousel
public carouselTile: NguCarousel;
constructor(private recentlyAddedService: RecentlyAddedService,
private imageService: ImageService) {}
public ngOnInit() {
this.getMovies();
this.getShows();
this.carouselTile = {
grid: {xs: 2, sm: 3, md: 3, lg: 5, all: 0},
slide: 2,
speed: 400,
animation: "lazy",
point: {
visible: true,
},
load: 2,
touch: true,
easing: "ease",
};
}
public close() {
if(this.range.length < 2) {
return;
}
if(!this.range[1]) {
// If we do not have a second date then just set it to now
this.range[1] = new Date();
}
this.getMovies();
}
public change() {
this.getShows();
}
private getShows() {
if(this.groupTv) {
this.recentlyAddedService.getRecentlyAddedTvGrouped().subscribe(x => {
this.tv = x;
this.tv.forEach((t) => {
this.imageService.getTvPoster(t.tvDbId).subscribe(p => {
t.posterPath = p;
});
});
});
} else {
this.recentlyAddedService.getRecentlyAddedTv().subscribe(x => {
this.tv = x;
this.tv.forEach((t) => {
this.imageService.getTvPoster(t.tvDbId).subscribe(p => {
t.posterPath = p;
});
});
});
}
}
private getMovies() {
this.recentlyAddedService.getRecentlyAddedMovies().subscribe(x => {
this.movies = x;
this.movies.forEach((movie) => {
if(movie.theMovieDbId) {
this.imageService.getMoviePoster(movie.theMovieDbId).subscribe(p => {
movie.posterPath = p;
});
} else if(movie.imdbId) {
this.imageService.getMoviePoster(movie.imdbId).subscribe(p => {
movie.posterPath = p;
});
} else {
movie.posterPath = "";
}
});
});
}
}

View file

@ -0,0 +1,47 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { OrderModule } from "ngx-order-pipe";
import { CalendarModule, PaginatorModule, SharedModule, TabViewModule } from "primeng/primeng";
import { IdentityService, ImageService, RecentlyAddedService } from "../services";
import { AuthGuard } from "../auth/auth.guard";
import { SharedModule as OmbiShared } from "../shared/shared.module";
import { RecentlyAddedComponent } from "./recentlyAdded.component";
import { NguCarouselModule } from "@ngu/carousel";
const routes: Routes = [
{ path: "", component: RecentlyAddedComponent, canActivate: [AuthGuard] },
];
@NgModule({
imports: [
RouterModule.forChild(routes),
NgbModule.forRoot(),
SharedModule,
OrderModule,
OmbiShared,
PaginatorModule,
TabViewModule,
CalendarModule,
NguCarouselModule,
],
declarations: [
RecentlyAddedComponent,
],
exports: [
RouterModule,
],
providers: [
IdentityService,
RecentlyAddedService,
ImageService,
],
})
export class RecentlyAddedModule { }

View file

@ -12,6 +12,7 @@ import {
IEmailNotificationSettings, IEmailNotificationSettings,
IEmbyServer, IEmbyServer,
IMattermostNotifcationSettings, IMattermostNotifcationSettings,
INewsletterNotificationSettings,
IPlexServer, IPlexServer,
IPushbulletNotificationSettings, IPushbulletNotificationSettings,
IPushoverNotificationSettings, IPushoverNotificationSettings,
@ -78,4 +79,7 @@ export class TesterService extends ServiceHelpers {
public sickrageTest(settings: ISickRageSettings): Observable<boolean> { public sickrageTest(settings: ISickRageSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}sickrage`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}sickrage`, JSON.stringify(settings), {headers: this.headers});
} }
public newsletterTest(settings: INewsletterNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}newsletter`, JSON.stringify(settings), {headers: this.headers});
}
} }

View file

@ -20,4 +20,10 @@ export class ImageService extends ServiceHelpers {
public getTvBanner(tvdbid: number): Observable<string> { public getTvBanner(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}tv/${tvdbid}`, {headers: this.headers}); return this.http.get<string>(`${this.url}tv/${tvdbid}`, {headers: this.headers});
} }
public getMoviePoster(themoviedbid: string): Observable<string> {
return this.http.get<string>(`${this.url}poster/movie/${themoviedbid}`, {headers: this.headers});
}
public getTvPoster(tvdbid: number): Observable<string> {
return this.http.get<string>(`${this.url}poster/tv/${tvdbid}`, {headers: this.headers});
}
} }

View file

@ -1,4 +1,4 @@
export * from "./applications"; export * from "./applications";
export * from "./helpers"; export * from "./helpers";
export * from "./identity.service"; export * from "./identity.service";
export * from "./image.service"; export * from "./image.service";
@ -13,3 +13,4 @@ export * from "./job.service";
export * from "./issues.service"; export * from "./issues.service";
export * from "./mobile.service"; export * from "./mobile.service";
export * from "./notificationMessage.service"; export * from "./notificationMessage.service";
export * from "./recentlyAdded.service";

View file

@ -0,0 +1,25 @@
import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx";
import { IRecentlyAddedMovies, IRecentlyAddedTvShows } from "./../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
export class RecentlyAddedService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/recentlyadded/", platformLocation);
}
public getRecentlyAddedMovies(): Observable<IRecentlyAddedMovies[]> {
return this.http.get<IRecentlyAddedMovies[]>(`${this.url}movies/`, {headers: this.headers});
}
public getRecentlyAddedTv(): Observable<IRecentlyAddedTvShows[]> {
return this.http.get<IRecentlyAddedTvShows[]>(`${this. url}tv/`, {headers: this.headers});
}
public getRecentlyAddedTvGrouped(): Observable<IRecentlyAddedTvShows[]> {
return this.http.get<IRecentlyAddedTvShows[]>(`${this.url}tv/grouped`, {headers: this.headers});
}
}

View file

@ -7,6 +7,8 @@ import {
IAbout, IAbout,
IAuthenticationSettings, IAuthenticationSettings,
ICouchPotatoSettings, ICouchPotatoSettings,
ICronTestModel,
ICronViewModelBody,
ICustomizationSettings, ICustomizationSettings,
IDiscordNotifcationSettings, IDiscordNotifcationSettings,
IDogNzbSettings, IDogNzbSettings,
@ -14,9 +16,11 @@ import {
IEmbySettings, IEmbySettings,
IIssueSettings, IIssueSettings,
IJobSettings, IJobSettings,
IJobSettingsViewModel,
ILandingPageSettings, ILandingPageSettings,
IMattermostNotifcationSettings, IMattermostNotifcationSettings,
IMobileNotifcationSettings, IMobileNotifcationSettings,
INewsletterNotificationSettings,
IOmbiSettings, IOmbiSettings,
IPlexSettings, IPlexSettings,
IPushbulletNotificationSettings, IPushbulletNotificationSettings,
@ -231,9 +235,14 @@ export class SettingsService extends ServiceHelpers {
return this.http.get<IJobSettings>(`${this.url}/jobs`, {headers: this.headers}); return this.http.get<IJobSettings>(`${this.url}/jobs`, {headers: this.headers});
} }
public saveJobSettings(settings: IJobSettings): Observable<boolean> { public saveJobSettings(settings: IJobSettings): Observable<IJobSettingsViewModel> {
return this.http return this.http
.post<boolean>(`${this.url}/jobs`, JSON.stringify(settings), {headers: this.headers}); .post<IJobSettingsViewModel>(`${this.url}/jobs`, JSON.stringify(settings), {headers: this.headers});
}
public testCron(body: ICronViewModelBody): Observable<ICronTestModel> {
return this.http
.post<ICronTestModel>(`${this.url}/testcron`, JSON.stringify(body), {headers: this.headers});
} }
public getSickRageSettings(): Observable<ISickRageSettings> { public getSickRageSettings(): Observable<ISickRageSettings> {
@ -257,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

@ -12,29 +12,34 @@
<label for="sonarrSync" class="control-label">Sonarr Sync</label> <label for="sonarrSync" class="control-label">Sonarr Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('sonarrSync').hasError('required')}" id="sonarrSync" name="sonarrSync" formControlName="sonarrSync"> <input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('sonarrSync').hasError('required')}" id="sonarrSync" name="sonarrSync" formControlName="sonarrSync">
<small *ngIf="form.get('sonarrSync').hasError('required')" class="error-text">The Sonarr Sync is required</small> <small *ngIf="form.get('sonarrSync').hasError('required')" class="error-text">The Sonarr Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('sonarrSync')?.value)">Test</button>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="sickRageSync" class="control-label">SickRage Sync</label> <label for="sickRageSync" class="control-label">SickRage Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('sonarrSync').hasError('required')}" id="sickRageSync" name="sickRageSync" formControlName="sickRageSync"> <input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('sonarrSync').hasError('required')}" id="sickRageSync" name="sickRageSync" formControlName="sickRageSync">
<small *ngIf="form.get('sickRageSync').hasError('required')" class="error-text">The SickRage Sync is required</small> <small *ngIf="form.get('sickRageSync').hasError('required')" class="error-text">The SickRage Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('sickRageSync')?.value)">Test</button>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="radarrSync" class="control-label">Radarr Sync</label> <label for="radarrSync" class="control-label">Radarr Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('radarrSync').hasError('required')}" id="radarrSync" name="radarrSync" formControlName="radarrSync"> <input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('radarrSync').hasError('required')}" id="radarrSync" name="radarrSync" formControlName="radarrSync">
<small *ngIf="form.get('radarrSync').hasError('required')" class="error-text">The Radarr Sync is required</small> <small *ngIf="form.get('radarrSync').hasError('required')" class="error-text">The Radarr Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('radarrSync')?.value)">Test</button>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="couchPotatoSync" class="control-label">CouchPotato Sync</label> <label for="couchPotatoSync" class="control-label">CouchPotato Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('radarrSync').hasError('required')}" id="couchPotatoSync" name="couchPotatoSync" formControlName="couchPotatoSync"> <input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('radarrSync').hasError('required')}" id="couchPotatoSync" name="couchPotatoSync" formControlName="couchPotatoSync">
<small *ngIf="form.get('couchPotatoSync').hasError('required')" class="error-text">The CouchPotato Sync is required</small> <small *ngIf="form.get('couchPotatoSync').hasError('required')" class="error-text">The CouchPotato Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('couchPotatoSync')?.value)">Test</button>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="automaticUpdater" class="control-label">Automatic Update</label> <label for="automaticUpdater" class="control-label">Automatic Update</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('automaticUpdater').hasError('required')}" id="automaticUpdater" name="automaticUpdater" formControlName="automaticUpdater"> <input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('automaticUpdater').hasError('required')}" id="automaticUpdater" name="automaticUpdater" formControlName="automaticUpdater">
<small *ngIf="form.get('automaticUpdater').hasError('required')" class="error-text">The Automatic Update is required</small> <small *ngIf="form.get('automaticUpdater').hasError('required')" class="error-text">The Automatic Update is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('automaticUpdater')?.value)">Test</button>
</div> </div>
@ -50,21 +55,45 @@
<label for="plexContentSync" class="control-label">Plex Sync</label> <label for="plexContentSync" class="control-label">Plex Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('plexContentSync').hasError('required')}" id="plexContentSync" name="plexContentSync" formControlName="plexContentSync"> <input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('plexContentSync').hasError('required')}" id="plexContentSync" name="plexContentSync" formControlName="plexContentSync">
<small *ngIf="form.get('plexContentSync').hasError('required')" class="error-text">The Plex Sync is required</small> <small *ngIf="form.get('plexContentSync').hasError('required')" class="error-text">The Plex Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('plexContentSync')?.value)">Test</button>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="embyContentSync" class="control-label">Emby Sync</label> <label for="embyContentSync" class="control-label">Emby Sync</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('embyContentSync').hasError('required')}" id="embyContentSync" name="embyContentSync" formControlName="embyContentSync"> <input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('embyContentSync').hasError('required')}" id="embyContentSync" name="embyContentSync" formControlName="embyContentSync">
<small *ngIf="form.get('embyContentSync').hasError('required')" class="error-text">The Emby Sync is required</small> <small *ngIf="form.get('embyContentSync').hasError('required')" class="error-text">The Emby Sync is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('embyContentSync')?.value)">Test</button>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="userImporter" class="control-label">User Importer</label> <label for="userImporter" class="control-label">User Importer</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('userImporter').hasError('required')}" id="userImporter" name="userImporter" formControlName="userImporter"> <input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('userImporter').hasError('required')}" id="userImporter" name="userImporter" formControlName="userImporter">
<small *ngIf="form.get('userImporter').hasError('required')" class="error-text">The User Importer is required</small> <small *ngIf="form.get('userImporter').hasError('required')" class="error-text">The User Importer is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('userImporter')?.value)">Test</button>
</div>
<div class="form-group">
<label for="userImporter" class="control-label">Refresh Metadata</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('refreshMetadata').hasError('required')}" id="refreshMetadata" name="refreshMetadata" formControlName="refreshMetadata">
<small *ngIf="form.get('refreshMetadata').hasError('required')" class="error-text">The Refresh Metadata is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('refreshMetadata')?.value)">Test</button>
</div>
<div class="form-group">
<label for="userImporter" class="control-label">Newsletter</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('newsletter').hasError('required')}" id="newsletter" name="newsletter" formControlName="newsletter">
<small *ngIf="form.get('newsletter').hasError('required')" class="error-text">The Newsletter is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('newsletter')?.value)">Test</button>
</div> </div>
</div> </div>
</form> </form>
</fieldset> </fieldset>
</div> </div>
<p-dialog header="CRON Schedule" [(visible)]="displayTest">
<ul *ngIf="testModel">
<li *ngFor="let item of testModel.schedule">{{item | date:'short'}}</li>
</ul>
</p-dialog>

View file

@ -1,7 +1,10 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { NotificationService, SettingsService } from "../../services"; import { NotificationService, SettingsService } from "../../services";
import { ICronTestModel } from "./../../interfaces";
@Component({ @Component({
templateUrl: "./jobs.component.html", templateUrl: "./jobs.component.html",
}) })
@ -10,6 +13,8 @@ export class JobsComponent implements OnInit {
public form: FormGroup; public form: FormGroup;
public profilesRunning: boolean; public profilesRunning: boolean;
public testModel: ICronTestModel;
public displayTest: boolean;
constructor(private readonly settingsService: SettingsService, constructor(private readonly settingsService: SettingsService,
private readonly fb: FormBuilder, private readonly fb: FormBuilder,
@ -26,10 +31,23 @@ export class JobsComponent implements OnInit {
sonarrSync: [x.radarrSync, Validators.required], sonarrSync: [x.radarrSync, Validators.required],
radarrSync: [x.sonarrSync, Validators.required], radarrSync: [x.sonarrSync, Validators.required],
sickRageSync: [x.sickRageSync, Validators.required], sickRageSync: [x.sickRageSync, Validators.required],
refreshMetadata: [x.refreshMetadata, Validators.required],
newsletter: [x.newsletter, Validators.required],
}); });
}); });
} }
public testCron(expression: string) {
this.settingsService.testCron({ expression }).subscribe(x => {
if(x.success) {
this.testModel = x;
this.displayTest = true;
} else {
this.notificationService.error(x.message);
}
});
}
public onSubmit(form: FormGroup) { public onSubmit(form: FormGroup) {
if (form.invalid) { if (form.invalid) {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
@ -37,10 +55,10 @@ export class JobsComponent implements OnInit {
} }
const settings = form.value; const settings = form.value;
this.settingsService.saveJobSettings(settings).subscribe(x => { this.settingsService.saveJobSettings(settings).subscribe(x => {
if (x) { if (x.result) {
this.notificationService.success("Successfully saved the job settings"); this.notificationService.success("Successfully saved the job settings");
} else { } else {
this.notificationService.success("There was an error when saving the job settings"); this.notificationService.error("There was an error when saving the job settings. " + x.message);
} }
}); });
} }

View file

@ -0,0 +1,42 @@
<settings-menu></settings-menu>
<div *ngIf="settings">
<fieldset>
<legend>Newsletter</legend>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enabled" [(ngModel)]="settings.enabled" ng-checked="settings.enabled"><label for="enabled">Enable</label>
</div>
</div>
<div class="form-group">
<label class="control-label">Subject</label>
<div>
<input type="text" class="form-control form-control-custom" [(ngModel)]="settings.notificationTemplate.subject" value="{{settings.notificationTemplate.subject}}">
</div>
</div>
<div class="form-group">
<label class="control-label">Message</label>
<div>
<textarea type="text" class="form-control form-control-custom" [(ngModel)]="settings.notificationTemplate.message" value="{{settings.notificationTemplate.message}}"></textarea>
</div>
</div>
<div class="form-group">
<div>
<button type="submit" id="save" (click)="onSubmit()" class="btn btn-primary-outline">Submit</button>
<button type="button" (click)="test()" class="btn btn-danger-outline">Test</button>
<button type="button" (click)="updateDatabase()" class="btn btn-info-outline" tooltipPosition="top" pTooltip="I recommend 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 everything that Ombi has found in your libraries will go out on the first email!">Update Database</button>
</div>
</div>
</div>
<div class="col-md-6">
</div>
</fieldset>
</div>

View file

@ -0,0 +1,45 @@

import { Component, OnInit } from "@angular/core";
import { INewsletterNotificationSettings, NotificationType } from "../../interfaces";
import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
import { TesterService } from "./../../services/applications/tester.service";
@Component({
templateUrl: "./newsletter.component.html",
})
export class NewsletterComponent implements OnInit {
public NotificationType = NotificationType;
public settings: INewsletterNotificationSettings;
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
private testService: TesterService) { }
public ngOnInit() {
this.settingsService.getNewsletterSettings().subscribe(x => {
this.settings = x;
});
}
public updateDatabase() {
this.settingsService.updateNewsletterDatabase().subscribe();
}
public test() {
this.testService.newsletterTest(this.settings).subscribe();
}
public onSubmit() {
this.settingsService.saveNewsletterSettings(this.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");
}
});
}
}

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";
@ -41,7 +42,7 @@ import { WikiComponent } from "./wiki.component";
import { SettingsMenuComponent } from "./settingsmenu.component"; import { SettingsMenuComponent } from "./settingsmenu.component";
import { AutoCompleteModule, CalendarModule, 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] },
@ -69,6 +70,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({
@ -88,6 +90,7 @@ const routes: Routes = [
ClipboardModule, ClipboardModule,
PipeModule, PipeModule,
RadioButtonModule, RadioButtonModule,
DialogModule,
], ],
declarations: [ declarations: [
SettingsMenuComponent, SettingsMenuComponent,
@ -118,6 +121,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

@ -10,12 +10,14 @@ using Ombi.Api.Radarr;
using Ombi.Api.SickRage; using Ombi.Api.SickRage;
using Ombi.Api.Sonarr; using Ombi.Api.Sonarr;
using Ombi.Attributes; using Ombi.Attributes;
using Ombi.Core.Models.UI;
using Ombi.Core.Notifications; using Ombi.Core.Notifications;
using Ombi.Core.Settings.Models.External; using Ombi.Core.Settings.Models.External;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Notifications; using Ombi.Notifications;
using Ombi.Notifications.Agents; using Ombi.Notifications.Agents;
using Ombi.Notifications.Models; using Ombi.Notifications.Models;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Settings.Settings.Models.External; using Ombi.Settings.Settings.Models.External;
using Ombi.Settings.Settings.Models.Notifications; using Ombi.Settings.Settings.Models.Notifications;
@ -35,7 +37,7 @@ namespace Ombi.Controllers.External
public TesterController(INotificationService service, IDiscordNotification notification, IEmailNotification emailN, public TesterController(INotificationService service, IDiscordNotification notification, IEmailNotification emailN,
IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm, IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm,
IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger<TesterController> log, IEmailProvider provider, IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger<TesterController> log, IEmailProvider provider,
ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi) ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter)
{ {
Service = service; Service = service;
DiscordNotification = notification; DiscordNotification = notification;
@ -53,6 +55,7 @@ namespace Ombi.Controllers.External
CouchPotatoApi = cpApi; CouchPotatoApi = cpApi;
TelegramNotification = telegram; TelegramNotification = telegram;
SickRageApi = srApi; SickRageApi = srApi;
Newsletter = newsletter;
} }
private INotificationService Service { get; } private INotificationService Service { get; }
@ -71,6 +74,7 @@ namespace Ombi.Controllers.External
private IEmailProvider EmailProvider { get; } private IEmailProvider EmailProvider { get; }
private ITelegramNotification TelegramNotification { get; } private ITelegramNotification TelegramNotification { get; }
private ISickRageApi SickRageApi { get; } private ISickRageApi SickRageApi { get; }
private INewsletterJob Newsletter { get; }
/// <summary> /// <summary>
@ -368,5 +372,21 @@ namespace Ombi.Controllers.External
return false; return false;
} }
} }
[HttpPost("newsletter")]
public async Task<bool> NewsletterTest([FromBody] NewsletterNotificationViewModel settings)
{
try
{
settings.Enabled = true;
await Newsletter.Start(settings, true);
return true;
}
catch (Exception e)
{
Log.LogError(LoggingEvents.Api, e, "Could not test Newsletter");
return false;
}
}
} }
} }

View file

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Ombi.Api.TheMovieDb;
using Ombi.Config; using Ombi.Config;
using Ombi.Helpers; using Ombi.Helpers;
@ -17,16 +18,16 @@ namespace Ombi.Controllers
[Produces("application/json")] [Produces("application/json")]
public class ImagesController : Controller public class ImagesController : Controller
{ {
public ImagesController(IFanartTvApi api, IApplicationConfigRepository config, public ImagesController(IFanartTvApi fanartTvApi, IApplicationConfigRepository config,
IOptions<LandingPageBackground> options, ICacheService c) IOptions<LandingPageBackground> options, ICacheService c)
{ {
Api = api; FanartTvApi = fanartTvApi;
Config = config; Config = config;
Options = options.Value; Options = options.Value;
_cache = c; _cache = c;
} }
private IFanartTvApi Api { get; } private IFanartTvApi FanartTvApi { get; }
private IApplicationConfigRepository Config { get; } private IApplicationConfigRepository Config { get; }
private LandingPageBackground Options { get; } private LandingPageBackground Options { get; }
private readonly ICacheService _cache; private readonly ICacheService _cache;
@ -36,15 +37,83 @@ namespace Ombi.Controllers
{ {
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.Get(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1)); var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.Get(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var images = await Api.GetTvImages(tvdbid, key.Value); var images = await FanartTvApi.GetTvImages(tvdbid, key.Value);
if (images == null)
{
return string.Empty;
}
if (images.tvbanner != null) if (images.tvbanner != null)
{ {
return images.tvbanner.FirstOrDefault()?.url ?? string.Empty; var enImage = images.tvbanner.Where(x => x.lang == "en").OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
} if (enImage == null)
if (images.showbackground != null)
{ {
return images.showbackground.FirstOrDefault()?.url ?? string.Empty; return images.tvbanner.OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
} }
}
if (images.seasonposter != null)
{
return images.seasonposter.FirstOrDefault()?.url ?? string.Empty;
}
return string.Empty;
}
[HttpGet("poster/movie/{movieDbId}")]
public async Task<string> GetMoviePoster(string movieDbId)
{
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.Get(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var images = await FanartTvApi.GetMovieImages(movieDbId, key.Value);
if (images == null)
{
return string.Empty;
}
if (images.movieposter?.Any() ?? false)
{
var enImage = images.movieposter.Where(x => x.lang == "en").OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
if (enImage == null)
{
return images.movieposter.OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
}
return enImage;
}
if (images.moviethumb?.Any() ?? false)
{
return images.moviethumb.OrderBy(x => x.likes).Select(x => x.url).FirstOrDefault();
}
return string.Empty;
}
[HttpGet("poster/tv/{tvdbid}")]
public async Task<string> GetTvPoster(int tvdbid)
{
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.Get(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var images = await FanartTvApi.GetTvImages(tvdbid, key.Value);
if (images == null)
{
return string.Empty;
}
if (images.tvposter?.Any() ?? false)
{
var enImage = images.tvposter.Where(x => x.lang == "en").OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
if (enImage == null)
{
return images.tvposter.OrderByDescending(x => x.likes).Select(x => x.url).FirstOrDefault();
}
return enImage;
}
if (images.tvthumb?.Any() ?? false)
{
return images.tvthumb.OrderBy(x => x.likes).Select(x => x.url).FirstOrDefault();
}
return string.Empty; return string.Empty;
} }
@ -63,11 +132,11 @@ namespace Ombi.Controllers
if (moviesArray.Any()) if (moviesArray.Any())
{ {
var item = rand.Next(moviesArray.Length); var item = rand.Next(moviesArray.Length);
var result = await Api.GetMovieImages(moviesArray[item], key.Value); var result = await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value);
while (!result.moviebackground.Any()) while (!result.moviebackground.Any())
{ {
result = await Api.GetMovieImages(moviesArray[item], key.Value); result = await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value);
} }
movieUrl = result.moviebackground[0].url; movieUrl = result.moviebackground[0].url;
@ -75,11 +144,11 @@ namespace Ombi.Controllers
if(tvArray.Any()) if(tvArray.Any())
{ {
var item = rand.Next(tvArray.Length); var item = rand.Next(tvArray.Length);
var result = await Api.GetTvImages(tvArray[item], key.Value); var result = await FanartTvApi.GetTvImages(tvArray[item], key.Value);
while (!result.showbackground.Any()) while (!result.showbackground.Any())
{ {
result = await Api.GetTvImages(tvArray[item], key.Value); result = await FanartTvApi.GetTvImages(tvArray[item], key.Value);
} }
tvUrl = result.showbackground[0].url; tvUrl = result.showbackground[0].url;

View file

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Core.Engine;
using Ombi.Core.Models;
using Ombi.Models;
namespace Ombi.Controllers
{
[ApiV1]
[Produces("application/json")]
[Authorize]
public class RecentlyAddedController : Controller
{
public RecentlyAddedController(IRecentlyAddedEngine engine)
{
_recentlyAdded = engine;
}
private readonly IRecentlyAddedEngine _recentlyAdded;
/// <summary>
/// Returns the recently added movies for the past 7 days
/// </summary>
[HttpGet("movies")]
[ProducesResponseType(typeof(IEnumerable<RecentlyAddedMovieModel>), 200)]
public IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies()
{
return _recentlyAdded.GetRecentlyAddedMovies(DateTime.UtcNow.AddDays(-7), DateTime.UtcNow);
}
/// <summary>
/// Returns the recently added tv shows for the past 7 days
/// </summary>
[HttpGet("tv")]
[ProducesResponseType(typeof(IEnumerable<RecentlyAddedMovieModel>), 200)]
public IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedShows()
{
return _recentlyAdded.GetRecentlyAddedTv(DateTime.UtcNow.AddDays(-7), DateTime.UtcNow, false);
}
/// <summary>
/// Returns the recently added tv shows for the past 7 days and groups them by season
/// </summary>
[HttpGet("tv/grouped")]
[ProducesResponseType(typeof(IEnumerable<RecentlyAddedMovieModel>), 200)]
public IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedShowsGrouped()
{
return _recentlyAdded.GetRecentlyAddedTv(DateTime.UtcNow.AddDays(-7), DateTime.UtcNow, true);
}
}
}

View file

@ -5,15 +5,18 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoMapper; using AutoMapper;
using Hangfire; using Hangfire;
using Hangfire.RecurringJobExtensions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.PlatformAbstractions; using Microsoft.Extensions.PlatformAbstractions;
using NCrontab;
using Ombi.Api.Emby; using Ombi.Api.Emby;
using Ombi.Attributes; using Ombi.Attributes;
using Ombi.Core.Models.UI; using Ombi.Core.Models.UI;
@ -29,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
{ {
@ -57,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;
@ -66,6 +71,7 @@ namespace Ombi.Controllers
_radarrSync = radarrSync; _radarrSync = radarrSync;
_cache = memCache; _cache = memCache;
_githubApi = githubApi; _githubApi = githubApi;
_recentlyAdded = engine;
} }
private ISettingsResolver SettingsResolver { get; } private ISettingsResolver SettingsResolver { get; }
@ -75,6 +81,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.
@ -465,6 +472,7 @@ namespace Ombi.Controllers
j.PlexContentSync = j.PlexContentSync.HasValue() ? j.PlexContentSync : JobSettingsHelper.PlexContent(j); j.PlexContentSync = j.PlexContentSync.HasValue() ? j.PlexContentSync : JobSettingsHelper.PlexContent(j);
j.UserImporter = j.UserImporter.HasValue() ? j.UserImporter : JobSettingsHelper.UserImporter(j); j.UserImporter = j.UserImporter.HasValue() ? j.UserImporter : JobSettingsHelper.UserImporter(j);
j.SickRageSync = j.SickRageSync.HasValue() ? j.SickRageSync : JobSettingsHelper.SickRageSync(j); j.SickRageSync = j.SickRageSync.HasValue() ? j.SickRageSync : JobSettingsHelper.SickRageSync(j);
j.RefreshMetadata = j.RefreshMetadata.HasValue() ? j.RefreshMetadata : JobSettingsHelper.RefreshMetadata(j);
return j; return j;
} }
@ -475,9 +483,71 @@ namespace Ombi.Controllers
/// <param name="settings">The settings.</param> /// <param name="settings">The settings.</param>
/// <returns></returns> /// <returns></returns>
[HttpPost("jobs")] [HttpPost("jobs")]
public async Task<bool> JobSettings([FromBody]JobSettings settings) public async Task<JobSettingsViewModel> JobSettings([FromBody]JobSettings settings)
{ {
return await Save(settings); // Verify that we have correct CRON's
foreach (var propertyInfo in settings.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (propertyInfo.Name.Equals("Id", StringComparison.CurrentCultureIgnoreCase))
{
continue;
}
var expression = (string)propertyInfo.GetValue(settings, null);
try
{
var r = CrontabSchedule.TryParse(expression);
if (r == null)
{
return new JobSettingsViewModel
{
Message = $"{propertyInfo.Name} does not have a valid CRON Expression"
};
}
}
catch (Exception)
{
return new JobSettingsViewModel
{
Message = $"{propertyInfo.Name} does not have a valid CRON Expression"
};
}
}
var result = await Save(settings);
return new JobSettingsViewModel
{
Result = result
};
}
[HttpPost("testcron")]
public CronTestModel TestCron([FromBody] CronViewModelBody body)
{
var model = new CronTestModel();
try
{
var time = DateTime.UtcNow;
var result = CrontabSchedule.TryParse(body.Expression);
for (int i = 0; i < 10; i++)
{
var next = result.GetNextOccurrence(time);
model.Schedule.Add(next);
time = next;
}
model.Success = true;
return model;
}
catch (Exception)
{
return new CronTestModel
{
Message = $"CRON Expression {body.Expression} is not valid"
};
}
} }
@ -541,7 +611,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;
} }
@ -588,7 +658,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;
} }
@ -623,7 +693,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;
} }
@ -657,7 +727,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;
} }
@ -691,7 +761,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;
} }
@ -726,7 +796,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;
} }
@ -760,7 +830,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;
} }
@ -794,17 +864,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

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace Ombi.Models
{
public class CronTestModel
{
public bool Success { get; set; }
public string Message { get; set; }
public List<DateTime> Schedule { get; set; } = new List<DateTime>();
}
}

View file

@ -0,0 +1,7 @@
namespace Ombi.Models
{
public class CronViewModelBody
{
public string Expression { get; set; }
}
}

View file

@ -0,0 +1,8 @@
namespace Ombi.Models
{
public class JobSettingsViewModel
{
public bool Result { get; set; }
public string Message { get; set; }
}
}

View file

@ -28,15 +28,19 @@
<ItemGroup> <ItemGroup>
<!-- Files not to show in IDE --> <!-- Files not to show in IDE -->
<Compile Remove="Logs\**" />
<Compile Remove="Styles\**" /> <Compile Remove="Styles\**" />
<Compile Remove="wwwroot\dist\**" /> <Compile Remove="wwwroot\dist\**" />
<!-- Files not to publish (note that the 'dist' subfolders are re-added below) --> <!-- Files not to publish (note that the 'dist' subfolders are re-added below) -->
<Content Remove="ClientApp\**" /> <Content Remove="ClientApp\**" />
<Content Remove="Logs\**" />
<Content Remove="Styles\**" /> <Content Remove="Styles\**" />
<Content Remove="wwwroot\dist\**" /> <Content Remove="wwwroot\dist\**" />
<EmbeddedResource Remove="Logs\**" />
<EmbeddedResource Remove="Styles\**" /> <EmbeddedResource Remove="Styles\**" />
<EmbeddedResource Remove="wwwroot\dist\**" /> <EmbeddedResource Remove="wwwroot\dist\**" />
<None Remove="Logs\**" />
<None Remove="Styles\**" /> <None Remove="Styles\**" />
<None Remove="wwwroot\dist\**" /> <None Remove="wwwroot\dist\**" />
</ItemGroup> </ItemGroup>
@ -66,6 +70,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.0.2" /> <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.0.2" />
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.0.0-alpha6-79" /> <PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.0.0-alpha6-79" />
<PackageReference Include="ncrontab" Version="3.3.0" />
<PackageReference Include="Serilog" Version="2.6.0-dev-00892" /> <PackageReference Include="Serilog" Version="2.6.0-dev-00892" />
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" /> <PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="3.2.0" /> <PackageReference Include="Serilog.Sinks.File" Version="3.2.0" />

File diff suppressed because it is too large Load diff

View file

@ -10,37 +10,38 @@
"restore": "dotnet restore && npm install" "restore": "dotnet restore && npm install"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^5.1.2", "@angular/animations": "^5.2.5",
"@angular/cdk": "^2.0.0-beta.12", "@angular/cdk": "^5.2.1",
"@angular/common": "^5.1.2", "@angular/common": "^5.2.5",
"@angular/compiler": "^5.1.2", "@angular/compiler": "^5.2.5",
"@angular/core": "^5.1.2", "@angular/core": "^5.2.5",
"@angular/forms": "^5.1.2", "@angular/forms": "^5.2.5",
"@angular/http": "^5.1.2", "@angular/http": "^5.2.5",
"@angular/material": "^2.0.0-beta.12", "@angular/material": "^5.2.1",
"@angular/platform-browser": "^5.1.2", "@angular/platform-browser": "^5.2.5",
"@angular/platform-browser-dynamic": "^5.1.2", "@angular/platform-browser-dynamic": "^5.2.5",
"@angular/platform-server": "5.0.0", "@angular/platform-server": "^5.2.5",
"@angular/router": "^5.1.2", "@angular/router": "^5.2.5",
"@auth0/angular-jwt": "1.0.0-beta.9", "@auth0/angular-jwt": "1.0.0-beta.9",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.8", "@ng-bootstrap/ng-bootstrap": "^1.0.0",
"@ngu/carousel": "^1.4.8",
"@ngx-translate/core": "^8.0.0", "@ngx-translate/core": "^8.0.0",
"@ngx-translate/http-loader": "^2.0.1", "@ngx-translate/http-loader": "^2.0.1",
"@types/core-js": "^0.9.43", "@types/core-js": "^0.9.46",
"@types/extract-text-webpack-plugin": "^3.0.0", "@types/extract-text-webpack-plugin": "^3.0.1",
"@types/intro.js": "^2.4.3", "@types/intro.js": "^2.4.3",
"@types/node": "^8.5.2", "@types/node": "^8.9.4",
"@types/webpack": "^3.8.1", "@types/webpack": "^3.8.7",
"angular-router-loader": "^0.8.1", "angular-router-loader": "^0.8.2",
"angular2-moment": "^1.7.1", "angular2-moment": "^1.8.0",
"angular2-template-loader": "^0.6.2", "angular2-template-loader": "^0.6.2",
"aspnet-webpack": "^2.0.1", "aspnet-webpack": "^2.0.3",
"awesome-typescript-loader": "^3.4.1", "awesome-typescript-loader": "^3.4.1",
"bootstrap": "3.3.7", "bootstrap": "3.3.7",
"bootswatch": "3.3.7", "bootswatch": "3.3.7",
"core-js": "^2.5.3", "core-js": "^2.5.3",
"css": "^2.2.1", "css": "^2.2.1",
"css-loader": "^0.28.7", "css-loader": "^0.28.9",
"del": "^3.0.0", "del": "^3.0.0",
"event-source-polyfill": "^0.0.11", "event-source-polyfill": "^0.0.11",
"expose-loader": "^0.7.4", "expose-loader": "^0.7.4",
@ -51,7 +52,7 @@
"gulp-run": "^1.7.1", "gulp-run": "^1.7.1",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"html-loader": "0.5.1", "html-loader": "0.5.1",
"jquery": "^3.2.1", "jquery": "^3.3.1",
"ng2-cookies": "^1.0.12", "ng2-cookies": "^1.0.12",
"ngx-clipboard": "8.1.1", "ngx-clipboard": "8.1.1",
"ngx-infinite-scroll": "^0.6.1", "ngx-infinite-scroll": "^0.6.1",
@ -61,22 +62,22 @@
"pace-progress": "^1.0.2", "pace-progress": "^1.0.2",
"primeng": "5.0.2", "primeng": "5.0.2",
"reflect-metadata": "0.1.10", "reflect-metadata": "0.1.10",
"run-sequence": "^2.2.0", "run-sequence": "^2.2.1",
"rxjs": "5.5.2", "rxjs": "5.5.2",
"sass-loader": "^6.0.6", "sass-loader": "^6.0.6",
"style-loader": "^0.19.1", "style-loader": "^0.19.1",
"to-string-loader": "^1.1.5", "to-string-loader": "^1.1.5",
"ts-node": "^3.3.0", "ts-node": "^3.3.0",
"tslint": "^5.8.0", "tslint": "^5.9.1",
"tslint-language-service": "^0.9.7", "tslint-language-service": "^0.9.8",
"typescript": "^2.6.2", "typescript": "^2.7.1",
"uglify-es": "^3.3.3", "uglify-es": "^3.3.10",
"uglifyjs-webpack-plugin": "^1.1.5", "uglifyjs-webpack-plugin": "^1.1.8",
"url-loader": "^0.6.2", "url-loader": "^0.6.2",
"webpack": "^3.10.0", "webpack": "^3.11.0",
"webpack-bundle-analyzer": "^2.9.1", "webpack-bundle-analyzer": "^2.10.0",
"webpack-hot-middleware": "^2.21.0", "webpack-hot-middleware": "^2.21.0",
"zone.js": "^0.8.19" "zone.js": "^0.8.20"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "4.0.4", "@types/chai": "4.0.4",

View file

@ -67,7 +67,8 @@
"Dutch": "Dutch", "Dutch": "Dutch",
"Norwegian":"Norwegian" "Norwegian":"Norwegian"
}, },
"OpenMobileApp":"Open Mobile App" "OpenMobileApp":"Open Mobile App",
"RecentlyAdded":"Recently Added"
}, },
"Search": { "Search": {
"Title": "Search", "Title": "Search",