From 5a228026638dec34377610921fbeecf17d3bdf6a Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 14 Feb 2018 08:11:52 +0000 Subject: [PATCH 01/42] Fixed the update check for the master build --- src/Ombi.Schedule/Processor/ChangeLogProcessor.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Ombi.Schedule/Processor/ChangeLogProcessor.cs b/src/Ombi.Schedule/Processor/ChangeLogProcessor.cs index 83d55aec3..f1c0925aa 100644 --- a/src/Ombi.Schedule/Processor/ChangeLogProcessor.cs +++ b/src/Ombi.Schedule/Processor/ChangeLogProcessor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using Octokit; using Ombi.Api; using Ombi.Api.Service; using Ombi.Core.Processor; +using Ombi.Helpers; namespace Ombi.Schedule.Processor { @@ -44,7 +46,8 @@ namespace Ombi.Schedule.Processor if (masterBranch) { 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 { @@ -173,10 +176,13 @@ namespace Ombi.Schedule.Processor var releases = await client.Repository.Release.GetAll("tidusjar", "ombi"); var latest = releases.FirstOrDefault(x => x.TagName == releaseTag); - + if (latest.Name.Contains("V2", CompareOptions.IgnoreCase)) + { + latest = null; + } if (latest == null) { - latest = releases.OrderBy(x => x.CreatedAt).FirstOrDefault(); + latest = releases.OrderByDescending(x => x.CreatedAt).FirstOrDefault(); } foreach (var item in latest.Assets) { From 109f5317bbe826e3ad0a00d0ba5b872e5ae952ca Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 14 Feb 2018 13:43:39 +0000 Subject: [PATCH 02/42] Added the recently added engine with some basic methods --- src/Ombi.Core/Engine/RecentlyAddedEngine.cs | 80 +++++++++++++++++++ .../Models/RecentlyAddedMovieModel.cs | 23 ++++++ .../Repository/EmbyContentRepository.cs | 4 +- .../Repository/IEmbyContentRepository.cs | 2 +- src/Ombi/Ombi.csproj | 4 + 5 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/Ombi.Core/Engine/RecentlyAddedEngine.cs create mode 100644 src/Ombi.Core/Models/RecentlyAddedMovieModel.cs diff --git a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs new file mode 100644 index 000000000..97c086e5b --- /dev/null +++ b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Ombi.Core.Models; +using Ombi.Store.Entities; +using Ombi.Store.Repository; + +namespace Ombi.Core.Engine +{ + public class RecentlyAddedEngine + { + public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby) + { + _plex = plex; + _emby = emby; + } + + private readonly IPlexContentRepository _plex; + private readonly IEmbyContentRepository _emby; + + public IEnumerable GetRecentlyAddedMovies(TimeSpan from) + { + var model = new HashSet(); + var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie && x.AddedAt.Subtract(from) <= x.AddedAt); + var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie && x.AddedAt.Subtract(from) <= x.AddedAt); + + TransformPlexMovies(plexMovies, model); + TransformEmbyMovies(embyMovies, model); + + return model; + } + + public IEnumerable GetRecentlyAddedMovies() + { + var model = new HashSet(); + var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie); + var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie); + + TransformPlexMovies(plexMovies, model); + TransformEmbyMovies(embyMovies, model); + + return model; + } + + private static void TransformEmbyMovies(IQueryable embyMovies, HashSet 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 plexMovies, HashSet 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 + }); + } + } + } +} diff --git a/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs b/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs new file mode 100644 index 000000000..c63ea98d3 --- /dev/null +++ b/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs @@ -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 + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Repository/EmbyContentRepository.cs b/src/Ombi.Store/Repository/EmbyContentRepository.cs index 519138edc..280243455 100644 --- a/src/Ombi.Store/Repository/EmbyContentRepository.cs +++ b/src/Ombi.Store/Repository/EmbyContentRepository.cs @@ -45,9 +45,9 @@ namespace Ombi.Store.Repository private IOmbiContext Db { get; } - public async Task> GetAll() + public IQueryable GetAll() { - return await Db.EmbyContent.ToListAsync(); + return Db.EmbyContent.AsQueryable(); } public async Task AddRange(IEnumerable content) diff --git a/src/Ombi.Store/Repository/IEmbyContentRepository.cs b/src/Ombi.Store/Repository/IEmbyContentRepository.cs index e6fe18067..3ed8d8abd 100644 --- a/src/Ombi.Store/Repository/IEmbyContentRepository.cs +++ b/src/Ombi.Store/Repository/IEmbyContentRepository.cs @@ -13,7 +13,7 @@ namespace Ombi.Store.Repository Task ContentExists(string providerId); IQueryable Get(); Task Get(string providerId); - Task> GetAll(); + IQueryable GetAll(); Task GetByEmbyId(string embyId); Task Update(EmbyContent existingContent); IQueryable GetAllEpisodes(); diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index 0f0dfb8c6..f55e6a507 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -28,15 +28,19 @@ + + + + From ee50eb79b7301147b3d121f69beb2413574530f4 Mon Sep 17 00:00:00 2001 From: Jamie Date: Thu, 15 Feb 2018 08:37:41 +0000 Subject: [PATCH 03/42] Added recently added stuff --- src/Ombi.Core/Engine/IRecentlyAddedEngine.cs | 12 +++++++ src/Ombi.Core/Engine/RecentlyAddedEngine.cs | 8 ++--- src/Ombi.DependencyInjection/IocExtensions.cs | 1 + .../Controllers/RecentlyAddedController.cs | 33 +++++++++++++++++++ src/Ombi/Models/RecentlyAddedRangeModel.cs | 10 ++++++ 5 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/Ombi.Core/Engine/IRecentlyAddedEngine.cs create mode 100644 src/Ombi/Controllers/RecentlyAddedController.cs create mode 100644 src/Ombi/Models/RecentlyAddedRangeModel.cs diff --git a/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs b/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs new file mode 100644 index 000000000..5b2988c8e --- /dev/null +++ b/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using Ombi.Core.Models; + +namespace Ombi.Core.Engine +{ + public interface IRecentlyAddedEngine + { + IEnumerable GetRecentlyAddedMovies(); + IEnumerable GetRecentlyAddedMovies(DateTime from, DateTime to); + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs index 97c086e5b..07762d336 100644 --- a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs +++ b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs @@ -11,7 +11,7 @@ using Ombi.Store.Repository; namespace Ombi.Core.Engine { - public class RecentlyAddedEngine + public class RecentlyAddedEngine : IRecentlyAddedEngine { public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby) { @@ -22,11 +22,11 @@ namespace Ombi.Core.Engine private readonly IPlexContentRepository _plex; private readonly IEmbyContentRepository _emby; - public IEnumerable GetRecentlyAddedMovies(TimeSpan from) + public IEnumerable GetRecentlyAddedMovies(DateTime from, DateTime to) { var model = new HashSet(); - var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie && x.AddedAt.Subtract(from) <= x.AddedAt); - var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie && x.AddedAt.Subtract(from) <= x.AddedAt); + 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); TransformPlexMovies(plexMovies, model); TransformEmbyMovies(embyMovies, model); diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 86d6da800..472c88c14 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -79,6 +79,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); } public static void RegisterHttp(this IServiceCollection services) diff --git a/src/Ombi/Controllers/RecentlyAddedController.cs b/src/Ombi/Controllers/RecentlyAddedController.cs new file mode 100644 index 000000000..c2574bee3 --- /dev/null +++ b/src/Ombi/Controllers/RecentlyAddedController.cs @@ -0,0 +1,33 @@ +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; + + /// + /// Returns the recently added movies between two dates + /// + [HttpPost("movies")] + [ProducesResponseType(typeof(IEnumerable), 200)] + public IEnumerable GetRecentlyAddedMovies([FromBody] RecentlyAddedRangeModel model) + { + return _recentlyAdded.GetRecentlyAddedMovies(model.From, model.To); + } + } +} \ No newline at end of file diff --git a/src/Ombi/Models/RecentlyAddedRangeModel.cs b/src/Ombi/Models/RecentlyAddedRangeModel.cs new file mode 100644 index 000000000..208f9cb49 --- /dev/null +++ b/src/Ombi/Models/RecentlyAddedRangeModel.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ombi.Models +{ + public class RecentlyAddedRangeModel + { + public DateTime From { get; set; } + public DateTime To { get; set; } + } +} \ No newline at end of file From d64e5ba23f0e928588556a340170d6f5f386d123 Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 9 Feb 2018 14:21:51 +0000 Subject: [PATCH 04/42] Update appveyor.yml --- appveyor.yml | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a9c94fc07..51a0f0859 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,5 @@ -version: 2.2.{build} +version: 3.0.{build} configuration: Release -<<<<<<< HEAD os: Visual Studio 2017 environment: nodejs_version: "7.8.0" @@ -23,33 +22,13 @@ after_build: appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\linux.tar.gz" -======= -assembly_info: - patch: true - file: '**\AssemblyInfo.*' - assembly_version: '2.2.1' - assembly_file_version: '{version}' - assembly_informational_version: '2.2.1' -before_build: -- cmd: appveyor-retry nuget restore -build: - verbosity: minimal -after_build: -- cmd: >- - 7z a Ombi.zip %APPVEYOR_BUILD_FOLDER%\Ombi.UI\bin\Release\ ->>>>>>> origin/master - appveyor PushArtifact Ombi.zip - -<<<<<<< HEAD appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\linux-arm.tar.gz" #cache: #- '%USERPROFILE%\.nuget\packages' -======= ->>>>>>> origin/master deploy: - provider: GitHub release: Ombi v$(appveyor_build_version) @@ -58,11 +37,3 @@ deploy: draft: true on: branch: master -<<<<<<< HEAD - -======= -- provider: Environment - name: Microserver - on: - branch: dev ->>>>>>> origin/master From 10b17143d18bd3088e0661b19a53633d67b8e742 Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 9 Feb 2018 14:22:13 +0000 Subject: [PATCH 05/42] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 21dadcafa..2684ccc3e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,24 +1,3 @@ - -<<<<<<< HEAD -======= - - - -!! Version 2.X is not supported anymore. Pleas don't open a issue for the 2.x version. -See https://github.com/tidusjar/Ombi/issues/1455 for more information. - - - - - - - - - - - - ->>>>>>> origin/master + + + + + + + + + +
+ + + + + + + +
+ +
+
+
+

Here is a list of Movies and TV Shows that have recently been added!

+ +
+ + {@RECENTLYADDED} + +
+ + + + + + + + +   + + + + \ No newline at end of file diff --git a/src/Ombi.Notifications/GenericEmailProvider.cs b/src/Ombi.Notifications/GenericEmailProvider.cs index e28b48c27..462f8918e 100644 --- a/src/Ombi.Notifications/GenericEmailProvider.cs +++ b/src/Ombi.Notifications/GenericEmailProvider.cs @@ -107,9 +107,13 @@ namespace Ombi.Notifications var body = new BodyBuilder { HtmlBody = model.Message, - TextBody = model.Other["PlainTextBody"] }; + if (model.Other.ContainsKey("PlainTextBody")) + { + body.TextBody = model.Other["PlainTextBody"]; + } + var message = new MimeMessage { Body = body.ToMessageBody(), diff --git a/src/Ombi.Schedule/JobSetup.cs b/src/Ombi.Schedule/JobSetup.cs index e73302387..3e8589fea 100644 --- a/src/Ombi.Schedule/JobSetup.cs +++ b/src/Ombi.Schedule/JobSetup.cs @@ -17,7 +17,8 @@ namespace Ombi.Schedule public JobSetup(IPlexContentSync plexContentSync, IRadarrSync radarrSync, IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter, IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache, - ISettingsService jobsettings, ISickRageSync srSync, IRefreshMetadata refresh) + ISettingsService jobsettings, ISickRageSync srSync, IRefreshMetadata refresh, + INewsletterJob newsletter) { _plexContentSync = plexContentSync; _radarrSync = radarrSync; @@ -30,6 +31,7 @@ namespace Ombi.Schedule _jobSettings = jobsettings; _srSync = srSync; _refreshMetadata = refresh; + _newsletter = newsletter; } private readonly IPlexContentSync _plexContentSync; @@ -43,6 +45,7 @@ namespace Ombi.Schedule private readonly ISickRageSync _srSync; private readonly ISettingsService _jobSettings; private readonly IRefreshMetadata _refreshMetadata; + private readonly INewsletterJob _newsletter; public void Setup() { @@ -60,6 +63,7 @@ namespace Ombi.Schedule RecurringJob.AddOrUpdate(() => _embyUserImporter.Start(), JobSettingsHelper.UserImporter(s)); RecurringJob.AddOrUpdate(() => _plexUserImporter.Start(), JobSettingsHelper.UserImporter(s)); + BackgroundJob.Enqueue(() => _newsletter.Start()); } } } diff --git a/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs b/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs new file mode 100644 index 000000000..61f4bd7c8 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs @@ -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("

{0}

", text, fontSize, fontWeight); + } + + protected virtual void AddImageInsideTable(StringBuilder sb, string url, int size = 400) + { + sb.Append(""); + sb.Append(""); + sb.Append($""); + sb.Append(""); + sb.Append(""); + } + + protected virtual void Href(StringBuilder sb, string url) + { + sb.AppendFormat("", url); + } + + protected virtual void TableData(StringBuilder sb) + { + sb.Append( + ""); + } + + protected virtual void EndTag(StringBuilder sb, string tag) + { + sb.AppendFormat("", tag); + } + + protected virtual void Header(StringBuilder sb, int size, string text, string fontWeight = "normal") + { + sb.AppendFormat( + "{1}", + size, text, fontWeight); + } + + + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Ombi/INewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/INewsletterJob.cs new file mode 100644 index 000000000..e5e587bec --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/INewsletterJob.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Ombi +{ + public interface INewsletterJob : IBaseJob + { + Task Start(); + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs new file mode 100644 index 000000000..544bbc6dd --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -0,0 +1,439 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +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 addedLog, + IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService custom, + ISettingsService emailSettings, INotificationTemplatesRepository templateRepo) + { + _plex = plex; + _emby = emby; + _recentlyAddedLog = addedLog; + _movieApi = movieApi; + _tvApi = tvApi; + _email = email; + _customizationSettings = custom; + _templateRepo = templateRepo; + _emailSettings = emailSettings; + } + + private readonly IPlexContentRepository _plex; + private readonly IEmbyContentRepository _emby; + private readonly IRepository _recentlyAddedLog; + private readonly IMovieDbApi _movieApi; + private readonly ITvMazeApi _tvApi; + private readonly IEmailProvider _email; + private readonly ISettingsService _customizationSettings; + private readonly INotificationTemplatesRepository _templateRepo; + private readonly ISettingsService _emailSettings; + + public async Task Start() + { + var template = await _templateRepo.GetTemplate(NotificationAgent.Email, NotificationType.Newsletter); + if (!template.Enabled) + { + return; + } + + var emailSettings = await _emailSettings.GetSettingsAsync(); + if (!ValidateConfiguration(emailSettings)) + { + return; + } + + // Get the Content + var plexContent = _plex.GetAll().Include(x => x.Episodes); + var embyContent = _emby.GetAll().Include(x => x.Episodes); + + var addedLog = _recentlyAddedLog.GetAll(); + var addedPlexLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex).Select(x => x.ContentId); + var addedEmbyLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby).Select(x => x.ContentId); + + // Filter out the ones that we haven't sent yet + var plexContentToSend = plexContent.Where(x => !addedPlexLogIds.Contains(x.Id)); + var embyContentToSend = embyContent.Where(x => !addedEmbyLogIds.Contains(x.Id)); + + var body = await BuildHtml(plexContentToSend, embyContentToSend); + + var email = new NewsletterTemplate(); + + var customization = await _customizationSettings.GetSettingsAsync(); + + var html = email.LoadTemplate(template.Subject, template.Message, body, customization.Logo); + + await _email.Send(new NotificationMessage {Message = html, Subject = template.Subject, To = "tidusjar@gmail.com"}, emailSettings); + } + + private async Task BuildHtml(IQueryable plexContentToSend, IQueryable embyContentToSend) + { + var sb = new StringBuilder(); + + sb.Append("

New Movies:



"); + await ProcessPlexMovies(plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie), sb); + await ProcessEmbyMovies(embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie), sb); + + sb.Append("

New Episodes:



"); + await ProcessPlexTv(plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Show), sb); + await ProcessEmbyMovies(embyContentToSend.Where(x => x.Type == EmbyMediaType.Series), sb); + + return sb.ToString(); + } + + private async Task ProcessPlexMovies(IQueryable plexContentToSend, StringBuilder sb) + { + sb.Append( + ""); + 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(""); + 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, StringBuilder sb) + { + sb.Append( + "
"); + 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(""); + 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 plexContent, StringBuilder sb) + { + var orderedTv = plexContent.OrderByDescending(x => x.AddedAt); + sb.Append( + "
"); + 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(""); + sb.Append( + "
"); + + 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("


"); + + } + + private async Task ProcessEmbyTv(IQueryable plexContent, StringBuilder sb) + { + var orderedTv = plexContent.OrderByDescending(x => x.AddedAt); + sb.Append( + ""); + 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(""); + sb.Append( + "
"); + + 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("


"); + } + + 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("
"); + sb.Append("
"); + sb.Append("
"); + sb.Append(""); + sb.Append(""); + } + + 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(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs index 5f5dd4635..225efb7d3 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs @@ -218,12 +218,6 @@ namespace Ombi.Schedule.Jobs.Ombi } return string.Empty; } - - - private async Task StartEmby() - { - - } private bool _disposed; @@ -236,6 +230,7 @@ namespace Ombi.Schedule.Jobs.Ombi { _plexRepo?.Dispose(); _embyRepo?.Dispose(); + _plexSettings?.Dispose(); } _disposed = true; } diff --git a/src/Ombi.Store/Context/IOmbiContext.cs b/src/Ombi.Store/Context/IOmbiContext.cs index 64f23669e..55d7db563 100644 --- a/src/Ombi.Store/Context/IOmbiContext.cs +++ b/src/Ombi.Store/Context/IOmbiContext.cs @@ -41,5 +41,6 @@ namespace Ombi.Store.Context DbSet SickRageCache { get; set; } DbSet SickRageEpisodeCache { get; set; } DbSet RequestLogs { get; set; } + DbSet RecentlyAddedLogs { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index 0ccf8ebe5..65c212d2b 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -15,7 +15,7 @@ namespace Ombi.Store.Context public OmbiContext() { if (_created) return; - + _created = true; Database.Migrate(); } @@ -37,6 +37,7 @@ namespace Ombi.Store.Context public DbSet IssueCategories { get; set; } public DbSet IssueComments { get; set; } public DbSet RequestLogs { get; set; } + public DbSet RecentlyAddedLogs { get; set; } public DbSet Audit { get; set; } @@ -55,7 +56,7 @@ namespace Ombi.Store.Context { i.StoragePath = string.Empty; } - optionsBuilder.UseSqlite($"Data Source={Path.Combine(i.StoragePath,"Ombi.db")}"); + optionsBuilder.UseSqlite($"Data Source={Path.Combine(i.StoragePath, "Ombi.db")}"); } protected override void OnModelCreating(ModelBuilder builder) @@ -70,7 +71,7 @@ namespace Ombi.Store.Context .WithMany(b => b.Episodes) .HasPrincipalKey(x => x.EmbyId) .HasForeignKey(p => p.ParentId); - + base.OnModelCreating(builder); } @@ -218,6 +219,16 @@ namespace Ombi.Store.Context break; case NotificationType.AdminNote: 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: throw new ArgumentOutOfRangeException(); } diff --git a/src/Ombi.Store/Entities/PlexServerContent.cs b/src/Ombi.Store/Entities/PlexServerContent.cs index f62dc7ff9..14028cb57 100644 --- a/src/Ombi.Store/Entities/PlexServerContent.cs +++ b/src/Ombi.Store/Entities/PlexServerContent.cs @@ -56,6 +56,15 @@ namespace Ombi.Store.Entities public int Key { get; set; } public DateTime AddedAt { 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")] diff --git a/src/Ombi.Store/Entities/RecentlyAddedLog.cs b/src/Ombi.Store/Entities/RecentlyAddedLog.cs new file mode 100644 index 000000000..52b047993 --- /dev/null +++ b/src/Ombi.Store/Entities/RecentlyAddedLog.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ombi.Store.Entities +{ + [Table("RecentlyAddedLog")] + public class RecentlyAddedLog : Entity + { + public RecentlyAddedType Type { get; set; } + public int ContentId { get; set; } // This is dependant on the type + public DateTime AddedAt { get; set; } + } + + public enum RecentlyAddedType + { + Plex, + Emby + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Migrations/20180322085345_RecentlyAddedLog.Designer.cs b/src/Ombi.Store/Migrations/20180322085345_RecentlyAddedLog.Designer.cs new file mode 100644 index 000000000..23f23bc4b --- /dev/null +++ b/src/Ombi.Store/Migrations/20180322085345_RecentlyAddedLog.Designer.cs @@ -0,0 +1,934 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using Ombi.Helpers; +using Ombi.Store.Context; +using Ombi.Store.Entities; +using Ombi.Store.Entities.Requests; +using System; + +namespace Ombi.Store.Migrations +{ + [DbContext(typeof(OmbiContext))] + [Migration("20180322085345_RecentlyAddedLog")] + partial class RecentlyAddedLog + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Type"); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("ApplicationConfiguration"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuditArea"); + + b.Property("AuditType"); + + b.Property("DateTime"); + + b.Property("Description"); + + b.Property("User"); + + b.HasKey("Id"); + + b.ToTable("Audit"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId") + .IsRequired(); + + b.Property("ProviderId"); + + b.Property("Title"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("EmbyId"); + + b.Property("EpisodeNumber"); + + b.Property("ParentId"); + + b.Property("ProviderId"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.Property("SettingsName"); + + b.HasKey("Id"); + + b.ToTable("GlobalSettings"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Agent"); + + b.Property("Enabled"); + + b.Property("Message"); + + b.Property("NotificationType"); + + b.Property("Subject"); + + b.HasKey("Id"); + + b.ToTable("NotificationTemplates"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("PlayerId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("Alias"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("EmbyConnectUserId"); + + b.Property("EpisodeRequestLimit"); + + b.Property("LastLoggedIn"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("MovieRequestLimit"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("ProviderUserId"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserAccessToken"); + + b.Property("UserName") + .HasMaxLength(256); + + b.Property("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("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("GrandparentKey"); + + b.Property("Key"); + + b.Property("ParentKey"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ParentKey"); + + b.Property("PlexContentId"); + + b.Property("PlexServerContentId"); + + b.Property("SeasonKey"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ImdbId"); + + b.Property("Key"); + + b.Property("Quality"); + + b.Property("ReleaseYear"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.Property("Type"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("HasFile"); + + b.Property("TheMovieDbId"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ContentId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("RecentlyAddedLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("IssueId"); + + b.Property("ParentRequestId"); + + b.Property("RequestType"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("SeriesType"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("ParentRequestId"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("ChildRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("IssueCategory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Comment"); + + b.Property("Date"); + + b.Property("IssuesId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("IssuesId"); + + b.HasIndex("UserId"); + + b.ToTable("IssueComments"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description"); + + b.Property("IssueCategoryId"); + + b.Property("IssueId"); + + b.Property("ProviderId"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("ResovledDate"); + + b.Property("Status"); + + b.Property("Subject"); + + b.Property("Title"); + + b.Property("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("Id") + .ValueGeneratedOnAdd(); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("Background"); + + b.Property("Denied"); + + b.Property("DeniedReason"); + + b.Property("DigitalReleaseDate"); + + b.Property("ImdbId"); + + b.Property("IssueId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RequestType"); + + b.Property("RequestedDate"); + + b.Property("RequestedUserId"); + + b.Property("RootPathOverride"); + + b.Property("Status"); + + b.Property("TheMovieDbId"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("RequestedUserId"); + + b.ToTable("MovieRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeCount"); + + b.Property("RequestDate"); + + b.Property("RequestId"); + + b.Property("RequestType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLog"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ImdbId"); + + b.Property("Overview"); + + b.Property("PosterPath"); + + b.Property("QualityOverride"); + + b.Property("ReleaseDate"); + + b.Property("RootFolder"); + + b.Property("Status"); + + b.Property("Title"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("TvRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("HasFile"); + + b.Property("SeasonNumber"); + + b.Property("TvDbId"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Token"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AirDate"); + + b.Property("Approved"); + + b.Property("Available"); + + b.Property("EpisodeNumber"); + + b.Property("Requested"); + + b.Property("SeasonId"); + + b.Property("Title"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("EpisodeRequests"); + }); + + modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChildRequestId"); + + b.Property("SeasonNumber"); + + b.HasKey("Id"); + + b.HasIndex("ChildRequestId"); + + b.ToTable("SeasonRequests"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Ombi.Store.Entities.OmbiUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", 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", 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 + } + } +} diff --git a/src/Ombi.Store/Migrations/20180322085345_RecentlyAddedLog.cs b/src/Ombi.Store/Migrations/20180322085345_RecentlyAddedLog.cs new file mode 100644 index 000000000..0e815a06f --- /dev/null +++ b/src/Ombi.Store/Migrations/20180322085345_RecentlyAddedLog.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Ombi.Store.Migrations +{ + public partial class RecentlyAddedLog : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RecentlyAddedLog", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AddedAt = table.Column(nullable: false), + ContentId = table.Column(nullable: false), + Type = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RecentlyAddedLog", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RecentlyAddedLog"); + } + } +} diff --git a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs index 35c5755da..7571c6f11 100644 --- a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs @@ -20,7 +20,7 @@ namespace Ombi.Store.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { @@ -430,6 +430,22 @@ namespace Ombi.Store.Migrations b.ToTable("RadarrCache"); }); + modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddedAt"); + + b.Property("ContentId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("RecentlyAddedLog"); + }); + modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => { b.Property("Id") diff --git a/src/Ombi/ClientApp/app/recentlyAdded/recentlyAdded.component.ts b/src/Ombi/ClientApp/app/recentlyAdded/recentlyAdded.component.ts index d540a6b2d..40025486f 100644 --- a/src/Ombi/ClientApp/app/recentlyAdded/recentlyAdded.component.ts +++ b/src/Ombi/ClientApp/app/recentlyAdded/recentlyAdded.component.ts @@ -79,19 +79,19 @@ export class RecentlyAddedComponent implements OnInit { this.recentlyAddedService.getRecentlyAddedTv().subscribe(x => { this.tv = x; - this.tv.forEach((t) => { - if(t.theMovieDbId) { - this.imageService.getTvPoster(t.imdbId).subscribe(p => { - t.posterPath = p; - }); - } else if(t.imdbId) { - this.imageService.getMoviePoster(t.imdbId).subscribe(p => { - t.posterPath = p; - }); - } else { - t.posterPath = ""; - } - }); + //this.tv.forEach((t) => { + // if(t.theMovieDbId) { + // this.imageService.getTvPoster(t.imdbId).subscribe(p => { + // t.posterPath = p; + // }); + // } else if(t.imdbId) { + // this.imageService.getMoviePoster(t.imdbId).subscribe(p => { + // t.posterPath = p; + // }); + // } else { + // t.posterPath = ""; + // } + //}); }); } diff --git a/src/Ombi/webpack.config.vendor.ts b/src/Ombi/webpack.config.vendor.ts index d4fe38c3a..1fdacb684 100644 --- a/src/Ombi/webpack.config.vendor.ts +++ b/src/Ombi/webpack.config.vendor.ts @@ -80,7 +80,7 @@ module.exports = (env: any) => { }, plugins: [ new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", Hammer: "hammerjs/hammer" }), // Global identifiers - new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/, path.join(__dirname, './client')), // Workaround for https://github.com/angular/angular/issues/20357 + new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/, path.join(__dirname,"./client")), // Workaround for https://github.com/angular/angular/issues/20357 new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/11580 new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, path.join(__dirname, "./ClientApp")), // Workaround for https://github.com/angular/angular/issues/14898 extractCSS, From 1528cdfc037336a2596301647dd3fac637a3127b Mon Sep 17 00:00:00 2001 From: Jamie Date: Thu, 22 Mar 2018 12:27:01 +0000 Subject: [PATCH 19/42] Pretty much finished the actual newsletter. Still need to work on the UI !wip --- src/Ombi.Core/Engine/IRecentlyAddedEngine.cs | 2 + src/Ombi.Core/Engine/RecentlyAddedEngine.cs | 68 +++++++- .../UI/NewsletterNotificationViewModel.cs | 23 +++ src/Ombi.Helpers/OmbiRoles.cs | 1 + .../NotificationMessageCurlys.cs | 7 + src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs | 154 ++++++++++++++++-- .../Notifications/NewsletterSettings.cs | 7 + src/Ombi.Store/Entities/RecentlyAddedLog.cs | 11 +- .../INotificationTemplatesRepository.cs | 5 +- .../NotificationTemplatesRepository.cs | 21 +++ .../app/interfaces/INotificationSettings.ts | 4 + .../app/services/settings.service.ts | 14 ++ .../notifications/newsletter.component.html | 52 ++++++ .../notifications/newsletter.component.ts | 71 ++++++++ src/Ombi/Controllers/SettingsController.cs | 47 +++++- 15 files changed, 463 insertions(+), 24 deletions(-) create mode 100644 src/Ombi.Core/Models/UI/NewsletterNotificationViewModel.cs create mode 100644 src/Ombi.Settings/Settings/Models/Notifications/NewsletterSettings.cs create mode 100644 src/Ombi/ClientApp/app/settings/notifications/newsletter.component.html create mode 100644 src/Ombi/ClientApp/app/settings/notifications/newsletter.component.ts diff --git a/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs b/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs index c119abbb4..3087ec829 100644 --- a/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs +++ b/src/Ombi.Core/Engine/IRecentlyAddedEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Ombi.Core.Models; namespace Ombi.Core.Engine @@ -10,5 +11,6 @@ namespace Ombi.Core.Engine IEnumerable GetRecentlyAddedMovies(DateTime from, DateTime to); IEnumerable GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason); IEnumerable GetRecentlyAddedTv(bool groupBySeason); + Task UpdateRecentlyAddedDatabase(); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs index b8cc0ee6b..61866af9d 100644 --- a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs +++ b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs @@ -1,27 +1,28 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Internal; 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) + public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository recentlyAdded) { _plex = plex; _emby = emby; + _recentlyAddedLog = recentlyAdded; } private readonly IPlexContentRepository _plex; private readonly IEmbyContentRepository _emby; + private readonly IRepository _recentlyAddedLog; public IEnumerable GetRecentlyAddedMovies(DateTime from, DateTime to) { @@ -55,6 +56,67 @@ namespace Ombi.Core.Engine return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason); } + public async Task UpdateRecentlyAddedDatabase() + { + var plexContent = _plex.GetAll().Include(x => x.Episodes); + var embyContent = _emby.GetAll().Include(x => x.Episodes); + var recentlyAddedLog = new HashSet(); + foreach (var p in plexContent) + { + if (p.Type == PlexMediaTypeEntity.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentId = p.Id + }); + } + else + { + // Add the episodes + foreach (var ep in p.Episodes) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentId = ep.Id + }); + } + } + } + + foreach (var e in embyContent) + { + if (e.Type == EmbyMediaType.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Emby, + ContentId = e.Id + }); + } + else + { + // Add the episodes + foreach (var ep in e.Episodes) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentId = ep.Id + }); + } + } + } + await _recentlyAddedLog.AddRange(recentlyAddedLog); + + return true; + } + private IEnumerable GetRecentlyAddedTv(IQueryable plexTv, IQueryable embyTv, bool groupBySeason) { diff --git a/src/Ombi.Core/Models/UI/NewsletterNotificationViewModel.cs b/src/Ombi.Core/Models/UI/NewsletterNotificationViewModel.cs new file mode 100644 index 000000000..a40044670 --- /dev/null +++ b/src/Ombi.Core/Models/UI/NewsletterNotificationViewModel.cs @@ -0,0 +1,23 @@ + +using System.Collections.Generic; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.UI +{ + /// + /// The view model for the notification settings page + /// + /// + public class NewsletterNotificationViewModel : NewsletterSettings + { + /// + /// Gets or sets the notification templates. + /// + /// + /// The notification templates. + /// + public NotificationTemplates NotificationTemplate { get; set; } + + } +} diff --git a/src/Ombi.Helpers/OmbiRoles.cs b/src/Ombi.Helpers/OmbiRoles.cs index 1b88b5d67..e7527279d 100644 --- a/src/Ombi.Helpers/OmbiRoles.cs +++ b/src/Ombi.Helpers/OmbiRoles.cs @@ -9,5 +9,6 @@ public const string RequestTv = nameof(RequestTv); public const string RequestMovie = nameof(RequestMovie); public const string Disabled = nameof(Disabled); + public const string RecievesNewsletter = nameof(RecievesNewsletter); } } \ No newline at end of file diff --git a/src/Ombi.Notifications/NotificationMessageCurlys.cs b/src/Ombi.Notifications/NotificationMessageCurlys.cs index d958cfc74..9dd3cc486 100644 --- a/src/Ombi.Notifications/NotificationMessageCurlys.cs +++ b/src/Ombi.Notifications/NotificationMessageCurlys.cs @@ -38,6 +38,13 @@ namespace Ombi.Notifications AdditionalInformation = opts?.AdditionalInformation ?? string.Empty; } + public void SetupNewsletter(CustomizationSettings s, string username) + { + ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty; + ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName; + RequestedUser = username; + } + public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s) { LoadIssues(opts); diff --git a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs index 544bbc6dd..693c8ae7a 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -1,7 +1,9 @@ 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; @@ -22,7 +24,8 @@ namespace Ombi.Schedule.Jobs.Ombi { public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository addedLog, IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService custom, - ISettingsService emailSettings, INotificationTemplatesRepository templateRepo) + ISettingsService emailSettings, INotificationTemplatesRepository templateRepo, + UserManager um, ISettingsService newsletter) { _plex = plex; _emby = emby; @@ -33,6 +36,8 @@ namespace Ombi.Schedule.Jobs.Ombi _customizationSettings = custom; _templateRepo = templateRepo; _emailSettings = emailSettings; + _newsletterSettings = newsletter; + _userManager = um; } private readonly IPlexContentRepository _plex; @@ -44,9 +49,16 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly ISettingsService _customizationSettings; private readonly INotificationTemplatesRepository _templateRepo; private readonly ISettingsService _emailSettings; + private readonly ISettingsService _newsletterSettings; + private readonly UserManager _userManager; public async Task Start() { + var newsletterSettings = await _newsletterSettings.GetSettingsAsync(); + if (!newsletterSettings.Enabled) + { + return; + } var template = await _templateRepo.GetTemplate(NotificationAgent.Email, NotificationType.Newsletter); if (!template.Enabled) { @@ -59,40 +71,146 @@ namespace Ombi.Schedule.Jobs.Ombi 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 addedPlexLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex).Select(x => x.ContentId); - var addedEmbyLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby).Select(x => x.ContentId); + 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 plexContentToSend = plexContent.Where(x => !addedPlexLogIds.Contains(x.Id)); - var embyContentToSend = embyContent.Where(x => !addedEmbyLogIds.Contains(x.Id)); + var plexContentMoviesToSend = plexContent.Where(x => !addedPlexMovieLogIds.Contains(x.Id)); + var embyContentMoviesToSend = embyContent.Where(x => !addedEmbyMoviesLogIds.Contains(x.Id)); + + var plexContentTvToSend = plexContent.Where(x => x.Episodes.Any(e => !addedPlexEpisodesLogIds.Contains(e.Id))); + var embyContentTvToSend = embyContent.Where(x => x.Episodes.Any(e => !addedEmbyEpisodesLogIds.Contains(e.Id))); + + var plexContentToSend = plexContentMoviesToSend.Union(plexContentTvToSend); + var embyContentToSend = embyContentMoviesToSend.Union(embyContentTvToSend); + var body = await BuildHtml(plexContentToSend, embyContentToSend); + // Get the users to send it to + var users = await _userManager.GetUsersInRoleAsync(OmbiRoles.RecievesNewsletter); + if (!users.Any()) + { + return; + } + var emailTasks = new List(); + foreach (var user in users) + { + if (user.Email.IsNullOrEmpty()) + { + continue; + } + + var html = LoadTemplate(body, template, customization, user.Alias); + + emailTasks.Add(_email.Send(new NotificationMessage { Message = html, Subject = template.Subject, To = user.Email }, emailSettings)); + } + + // Now add all of this to the Recently Added log + var recentlyAddedLog = new HashSet(); + foreach (var p in plexContentMoviesToSend) + { + if (p.Type == PlexMediaTypeEntity.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentId = p.Id + }); + } + else + { + // Add the episodes + foreach (var ep in p.Episodes) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentId = ep.Id + }); + } + } + } + + foreach (var e in embyContentMoviesToSend) + { + if (e.Type == EmbyMediaType.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Emby, + ContentId = e.Id + }); + } + else + { + // Add the episodes + foreach (var ep in e.Episodes) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Plex, + ContentId = ep.Id + }); + } + } + } + await _recentlyAddedLog.AddRange(recentlyAddedLog); + + await Task.WhenAll(emailTasks.ToArray()); + } + + private string LoadTemplate(string body, NotificationTemplates template, CustomizationSettings settings, string username) + { var email = new NewsletterTemplate(); - var customization = await _customizationSettings.GetSettingsAsync(); + var resolver = new NotificationMessageResolver(); + var curlys = new NotificationMessageCurlys(); + + curlys.SetupNewsletter(settings, username); - var html = email.LoadTemplate(template.Subject, template.Message, body, customization.Logo); + var parsed = resolver.ParseMessage(template, curlys); - await _email.Send(new NotificationMessage {Message = html, Subject = template.Subject, To = "tidusjar@gmail.com"}, emailSettings); + var html = email.LoadTemplate(parsed.Subject, parsed.Message, body, settings.Logo); + + return html; } private async Task BuildHtml(IQueryable plexContentToSend, IQueryable embyContentToSend) { var sb = new StringBuilder(); - sb.Append("

New Movies:



"); - await ProcessPlexMovies(plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie), sb); - await ProcessEmbyMovies(embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie), sb); + 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("

New Movies:



"); + await ProcessPlexMovies(plexMovies, sb); + await ProcessEmbyMovies(embyMovies, sb); + } - sb.Append("

New Episodes:



"); - await ProcessPlexTv(plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Show), sb); - await ProcessEmbyMovies(embyContentToSend.Where(x => x.Type == EmbyMediaType.Series), 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("

New Episodes:



"); + await ProcessPlexTv(plexTv, sb); + await ProcessEmbyMovies(embyTv, sb); + } return sb.ToString(); } @@ -330,7 +448,7 @@ namespace Ombi.Schedule.Jobs.Ombi sb.Append(""); sb.Append( ""); - + Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); Header(sb, 3, t.Title); EndTag(sb, "a"); @@ -426,6 +544,12 @@ namespace Ombi.Schedule.Jobs.Ombi { _plex?.Dispose(); _emby?.Dispose(); + _newsletterSettings?.Dispose(); + _customizationSettings?.Dispose(); + _emailSettings.Dispose(); + _recentlyAddedLog.Dispose(); + _templateRepo?.Dispose(); + _userManager?.Dispose(); } _disposed = true; } diff --git a/src/Ombi.Settings/Settings/Models/Notifications/NewsletterSettings.cs b/src/Ombi.Settings/Settings/Models/Notifications/NewsletterSettings.cs new file mode 100644 index 000000000..380e2d743 --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/Notifications/NewsletterSettings.cs @@ -0,0 +1,7 @@ +namespace Ombi.Settings.Settings.Models.Notifications +{ + public class NewsletterSettings : Settings + { + public bool Enabled { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Entities/RecentlyAddedLog.cs b/src/Ombi.Store/Entities/RecentlyAddedLog.cs index 52b047993..ba26eb566 100644 --- a/src/Ombi.Store/Entities/RecentlyAddedLog.cs +++ b/src/Ombi.Store/Entities/RecentlyAddedLog.cs @@ -7,13 +7,20 @@ namespace Ombi.Store.Entities 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, - Emby + Plex = 0, + Emby = 1 + } + + public enum ContentType + { + Parent = 0, + Episode = 1 } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs b/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs index 861c6b9bd..2ef0e09cf 100644 --- a/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs +++ b/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Ombi.Helpers; @@ -6,7 +7,7 @@ using Ombi.Store.Entities; namespace Ombi.Store.Repository { - public interface INotificationTemplatesRepository + public interface INotificationTemplatesRepository : IDisposable { IQueryable All(); Task> GetAllTemplates(); diff --git a/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs b/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs index e4b484967..de892e934 100644 --- a/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs +++ b/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs @@ -60,5 +60,26 @@ namespace Ombi.Store.Repository await Db.SaveChangesAsync().ConfigureAwait(false); 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); + } } } \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/interfaces/INotificationSettings.ts b/src/Ombi/ClientApp/app/interfaces/INotificationSettings.ts index 4446f6340..67a2594d2 100644 --- a/src/Ombi/ClientApp/app/interfaces/INotificationSettings.ts +++ b/src/Ombi/ClientApp/app/interfaces/INotificationSettings.ts @@ -54,6 +54,10 @@ export interface IDiscordNotifcationSettings extends INotificationSettings { notificationTemplates: INotificationTemplates[]; } +export interface INewsletterNotificationSettings extends INotificationSettings { + notificationTemplate: INotificationTemplates; +} + export interface ITelegramNotifcationSettings extends INotificationSettings { botApi: string; chatId: string; diff --git a/src/Ombi/ClientApp/app/services/settings.service.ts b/src/Ombi/ClientApp/app/services/settings.service.ts index 54f3458d5..f6f770a19 100644 --- a/src/Ombi/ClientApp/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/app/services/settings.service.ts @@ -20,6 +20,7 @@ import { ILandingPageSettings, IMattermostNotifcationSettings, IMobileNotifcationSettings, + INewsletterNotificationSettings, IOmbiSettings, IPlexSettings, IPushbulletNotificationSettings, @@ -265,4 +266,17 @@ export class SettingsService extends ServiceHelpers { return this.http .post(`${this.url}/issues`, JSON.stringify(settings), {headers: this.headers}); } + + public getNewsletterSettings(): Observable { + return this.http.get(`${this.url}/notifications/newsletter`, {headers: this.headers}); + } + + public updateNewsletterDatabase(): Observable { + return this.http.post(`${this.url}/notifications/newsletterdatabase`, {headers: this.headers}); + } + + public saveNewsletterSettings(settings: INewsletterNotificationSettings): Observable { + return this.http + .post(`${this.url}/notifications/newsletter`, JSON.stringify(settings), {headers: this.headers}); + } } diff --git a/src/Ombi/ClientApp/app/settings/notifications/newsletter.component.html b/src/Ombi/ClientApp/app/settings/notifications/newsletter.component.html new file mode 100644 index 000000000..5f663dd20 --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/newsletter.component.html @@ -0,0 +1,52 @@ + + +
+
+ Newsletter +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ + +
+
+
+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/notifications/newsletter.component.ts b/src/Ombi/ClientApp/app/settings/notifications/newsletter.component.ts new file mode 100644 index 000000000..7742e6f4e --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/newsletter.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +import { INewsletterNotificationSettings, INotificationTemplates, NotificationType } from "../../interfaces"; +import { TesterService } from "../../services"; +import { NotificationService } from "../../services"; +import { SettingsService } from "../../services"; + +@Component({ + templateUrl: "./newsletter.component.html", +}) +export class NewsletterComponent implements OnInit { + + public NotificationType = NotificationType; + public template: INotificationTemplates; + public form: FormGroup; + + constructor(private settingsService: SettingsService, + private notificationService: NotificationService, + private fb: FormBuilder, + private testerService: TesterService) { } + + public ngOnInit() { + this.settingsService.getNewsletterSettings().subscribe(x => { + this.template = x.notificationTemplate; + + this.form = this.fb.group({ + enabled: [x.enabled], + }); + }); + } + + public updateDatabase() { + this.settingsService.updateNewsletterDatabase().subscribe(); + } + + public onSubmit(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + const settings = form.value; + settings.notificationTemplate = this.template; + + this.settingsService.saveNewsletterSettings(settings).subscribe(x => { + if (x) { + this.notificationService.success("Successfully saved the Newsletter settings"); + } else { + this.notificationService.error("There was an error when saving the Newsletter settings"); + } + }); + + } + + public test(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + this.testerService.discordTest(form.value).subscribe(x => { + if (x) { + this.notificationService.success("Successfully sent a Discord message, please check the discord channel"); + } else { + this.notificationService.error("There was an error when sending the Discord message. Please check your settings"); + } + }); + + } +} diff --git a/src/Ombi/Controllers/SettingsController.cs b/src/Ombi/Controllers/SettingsController.cs index c0148c1bb..7970252a3 100644 --- a/src/Ombi/Controllers/SettingsController.cs +++ b/src/Ombi/Controllers/SettingsController.cs @@ -32,6 +32,7 @@ using Ombi.Settings.Settings.Models.Notifications; using Ombi.Store.Entities; using Ombi.Store.Repository; using Ombi.Api.Github; +using Ombi.Core.Engine; namespace Ombi.Controllers { @@ -60,7 +61,8 @@ namespace Ombi.Controllers IEmbyApi embyApi, IRadarrSync radarrSync, ICacheService memCache, - IGithubApi githubApi) + IGithubApi githubApi, + IRecentlyAddedEngine engine) { SettingsResolver = resolver; Mapper = mapper; @@ -78,6 +80,7 @@ namespace Ombi.Controllers private readonly IRadarrSync _radarrSync; private readonly ICacheService _cache; private readonly IGithubApi _githubApi; + private readonly IRecentlyAddedEngine _recentlyAdded; ///
/// Gets the Ombi settings. @@ -865,13 +868,53 @@ namespace Ombi.Controllers return model; } + /// + /// Saves the Newsletter notification settings. + /// + /// The model. + /// + [HttpPost("notifications/newsletter")] + public async Task NewsletterSettings([FromBody] NewsletterNotificationViewModel model) + { + // Save the email settings + var settings = Mapper.Map(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 UpdateNewsletterDatabase() + { + return await _recentlyAdded.UpdateRecentlyAddedDatabase(); + } + + /// + /// Gets the Newsletter Notification Settings. + /// + /// + [HttpGet("notifications/newsletter")] + public async Task NewsletterSettings() + { + var settings = await Get(); + var model = Mapper.Map(settings); + + // Lookup to see if we have any templates saved + var templates = await BuildTemplates(NotificationAgent.Email); + model.NotificationTemplate = templates.FirstOrDefault(x => x.NotificationType == NotificationType.Newsletter); + return model; + } + private async Task> BuildTemplates(NotificationAgent agent) { var templates = await TemplateRepository.GetAllTemplates(agent); return templates.OrderBy(x => x.NotificationType.ToString()).ToList(); } - private async Task Get() { var settings = SettingsResolver.Resolve(); From b99a5a668bedc550054c72fec1e29ebae32ad81b Mon Sep 17 00:00:00 2001 From: Jamie Date: Thu, 22 Mar 2018 13:00:25 +0000 Subject: [PATCH 20/42] Think i've finished the ui now too !wip needs testing --- src/Ombi.Mapping/Profiles/SettingsProfile.cs | 1 + .../Settings/Models/CustomizationSettings.cs | 1 + .../INotificationTemplatesRepository.cs | 4 +-- .../NotificationTemplatesRepository.cs | 8 +++--- src/Ombi/ClientApp/app/app.component.html | 15 ++++++----- .../app/interfaces/INotificationSettings.ts | 1 + .../ClientApp/app/interfaces/ISettings.ts | 1 + .../customization.component.html | 7 +++++ .../ClientApp/app/settings/settings.module.ts | 4 +++ .../app/settings/settingsmenu.component.html | 2 +- src/Ombi/Controllers/SettingsController.cs | 27 +++++++++++-------- 11 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/Ombi.Mapping/Profiles/SettingsProfile.cs b/src/Ombi.Mapping/Profiles/SettingsProfile.cs index 62232ee19..139290f2b 100644 --- a/src/Ombi.Mapping/Profiles/SettingsProfile.cs +++ b/src/Ombi.Mapping/Profiles/SettingsProfile.cs @@ -18,6 +18,7 @@ namespace Ombi.Mapping.Profiles CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } } \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs b/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs index 16d6245f5..515c2fc85 100644 --- a/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs +++ b/src/Ombi.Settings/Settings/Models/CustomizationSettings.cs @@ -18,6 +18,7 @@ namespace Ombi.Settings.Settings.Models public string PresetThemeName { get; set; } public string PresetThemeContent { get; set; } + public bool RecentlyAddedPage { get; set; } [NotMapped] public string PresetThemeVersion diff --git a/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs b/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs index 2ef0e09cf..2398158db 100644 --- a/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs +++ b/src/Ombi.Store/Repository/INotificationTemplatesRepository.cs @@ -10,8 +10,8 @@ namespace Ombi.Store.Repository public interface INotificationTemplatesRepository : IDisposable { IQueryable All(); - Task> GetAllTemplates(); - Task> GetAllTemplates(NotificationAgent agent); + IQueryable GetAllTemplates(); + IQueryable GetAllTemplates(NotificationAgent agent); Task Insert(NotificationTemplates entity); Task Update(NotificationTemplates template); Task UpdateRange(IEnumerable template); diff --git a/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs b/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs index de892e934..3822f8001 100644 --- a/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs +++ b/src/Ombi.Store/Repository/NotificationTemplatesRepository.cs @@ -23,14 +23,14 @@ namespace Ombi.Store.Repository return Db.NotificationTemplates.AsQueryable(); } - public async Task> GetAllTemplates() + public IQueryable GetAllTemplates() { - return await Db.NotificationTemplates.ToListAsync(); + return Db.NotificationTemplates; } - public async Task> GetAllTemplates(NotificationAgent agent) + public IQueryable GetAllTemplates(NotificationAgent agent) { - return await Db.NotificationTemplates.Where(x => x.Agent == agent).ToListAsync(); + return Db.NotificationTemplates.Where(x => x.Agent == agent); } public async Task GetTemplate(NotificationAgent agent, NotificationType type) diff --git a/src/Ombi/ClientApp/app/app.component.html b/src/Ombi/ClientApp/app/app.component.html index bc44c3b49..0db13fa9c 100644 --- a/src/Ombi/ClientApp/app/app.component.html +++ b/src/Ombi/ClientApp/app/app.component.html @@ -34,13 +34,14 @@ {{ 'NavigationBar.Requests' | translate }} - - +