|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Microsoft.AspNetCore.Identity;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using MimeKit;
|
|
|
|
|
using Ombi.Api.Lidarr;
|
|
|
|
|
using Ombi.Api.Lidarr.Models;
|
|
|
|
|
using Ombi.Api.TheMovieDb;
|
|
|
|
|
using Ombi.Api.TheMovieDb.Models;
|
|
|
|
|
using Ombi.Core.Settings;
|
|
|
|
|
using Ombi.Core.Settings.Models.External;
|
|
|
|
|
using Ombi.Helpers;
|
|
|
|
|
using Ombi.Hubs;
|
|
|
|
|
using Ombi.I18n.Resources;
|
|
|
|
|
using Ombi.Notifications;
|
|
|
|
|
using Ombi.Notifications.Models;
|
|
|
|
|
using Ombi.Notifications.Templates;
|
|
|
|
|
using Ombi.Settings.Settings.Models;
|
|
|
|
|
using Ombi.Settings.Settings.Models.External;
|
|
|
|
|
using Ombi.Settings.Settings.Models.Notifications;
|
|
|
|
|
using Ombi.Store.Entities;
|
|
|
|
|
using Ombi.Store.Repository;
|
|
|
|
|
using Quartz;
|
|
|
|
|
using ContentType = Ombi.Store.Entities.ContentType;
|
|
|
|
|
|
|
|
|
|
namespace Ombi.Schedule.Jobs.Ombi
|
|
|
|
|
{
|
|
|
|
|
public class NewsletterJob : HtmlTemplateGenerator, INewsletterJob
|
|
|
|
|
{
|
|
|
|
|
public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IJellyfinContentRepository jellyfin, IRepository<RecentlyAddedLog> addedLog,
|
|
|
|
|
IMovieDbApi movieApi, IEmailProvider email, ISettingsService<CustomizationSettings> custom,
|
|
|
|
|
ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo,
|
|
|
|
|
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter, ILogger<NewsletterJob> log,
|
|
|
|
|
ILidarrApi lidarrApi, IExternalRepository<LidarrAlbumCache> albumCache, ISettingsService<LidarrSettings> lidarrSettings,
|
|
|
|
|
ISettingsService<OmbiSettings> ombiSettings, ISettingsService<PlexSettings> plexSettings, ISettingsService<EmbySettings> embySettings, ISettingsService<JellyfinSettings> jellyfinSettings,
|
|
|
|
|
INotificationHubService notification, IRefreshMetadata refreshMetadata)
|
|
|
|
|
{
|
|
|
|
|
_plex = plex;
|
|
|
|
|
_emby = emby;
|
|
|
|
|
_jellyfin = jellyfin;
|
|
|
|
|
_recentlyAddedLog = addedLog;
|
|
|
|
|
_movieApi = movieApi;
|
|
|
|
|
_email = email;
|
|
|
|
|
_customizationSettings = custom;
|
|
|
|
|
_templateRepo = templateRepo;
|
|
|
|
|
_emailSettings = emailSettings;
|
|
|
|
|
_newsletterSettings = newsletter;
|
|
|
|
|
_userManager = um;
|
|
|
|
|
_log = log;
|
|
|
|
|
_lidarrApi = lidarrApi;
|
|
|
|
|
_lidarrAlbumRepository = albumCache;
|
|
|
|
|
_lidarrSettings = lidarrSettings;
|
|
|
|
|
_ombiSettings = ombiSettings;
|
|
|
|
|
_plexSettings = plexSettings;
|
|
|
|
|
_embySettings = embySettings;
|
|
|
|
|
_jellyfinSettings = jellyfinSettings;
|
|
|
|
|
_notification = notification;
|
|
|
|
|
_ombiSettings.ClearCache();
|
|
|
|
|
_plexSettings.ClearCache();
|
|
|
|
|
_emailSettings.ClearCache();
|
|
|
|
|
_customizationSettings.ClearCache();
|
|
|
|
|
_refreshMetadata = refreshMetadata;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private readonly IMediaServerContentRepository<PlexServerContent> _plex;
|
|
|
|
|
private readonly IMediaServerContentRepository<EmbyContent> _emby;
|
|
|
|
|
private readonly IMediaServerContentRepository<JellyfinContent> _jellyfin;
|
|
|
|
|
private readonly IRepository<RecentlyAddedLog> _recentlyAddedLog;
|
|
|
|
|
private readonly IMovieDbApi _movieApi;
|
|
|
|
|
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 ISettingsService<OmbiSettings> _ombiSettings;
|
|
|
|
|
private readonly UserManager<OmbiUser> _userManager;
|
|
|
|
|
private readonly ILogger _log;
|
|
|
|
|
private readonly ILidarrApi _lidarrApi;
|
|
|
|
|
private readonly IExternalRepository<LidarrAlbumCache> _lidarrAlbumRepository;
|
|
|
|
|
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
|
|
|
|
|
private readonly ISettingsService<PlexSettings> _plexSettings;
|
|
|
|
|
private readonly ISettingsService<EmbySettings> _embySettings;
|
|
|
|
|
private readonly ISettingsService<JellyfinSettings> _jellyfinSettings;
|
|
|
|
|
private readonly INotificationHubService _notification;
|
|
|
|
|
private readonly IRefreshMetadata _refreshMetadata;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await _notification.SendNotificationToAdmins("Newsletter Started");
|
|
|
|
|
var emailSettings = await _emailSettings.GetSettingsAsync();
|
|
|
|
|
if (!ValidateConfiguration(emailSettings))
|
|
|
|
|
{
|
|
|
|
|
await _notification.SendNotificationToAdmins("Newsletter Email Settings Not Configured");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
var plexSettings = await _plexSettings.GetSettingsAsync();
|
|
|
|
|
var embySettings = await _embySettings.GetSettingsAsync();
|
|
|
|
|
var jellyfinSettings = await _jellyfinSettings.GetSettingsAsync();
|
|
|
|
|
|
|
|
|
|
var customization = await _customizationSettings.GetSettingsAsync();
|
|
|
|
|
|
|
|
|
|
var moviesContents = new List<IMediaServerContent>();
|
|
|
|
|
var seriesContents = new List<IMediaServerEpisode>();
|
|
|
|
|
if (plexSettings.Enable)
|
|
|
|
|
{
|
|
|
|
|
moviesContents.AddRange(await GetMoviesContent(_plex, test));
|
|
|
|
|
seriesContents.AddRange(GetSeriesContent(_plex, test));
|
|
|
|
|
}
|
|
|
|
|
if (embySettings.Enable)
|
|
|
|
|
{
|
|
|
|
|
moviesContents.AddRange(await GetMoviesContent(_emby, test));
|
|
|
|
|
seriesContents.AddRange(GetSeriesContent(_emby, test));
|
|
|
|
|
}
|
|
|
|
|
if (jellyfinSettings.Enable)
|
|
|
|
|
{
|
|
|
|
|
moviesContents.AddRange(await GetMoviesContent(_jellyfin, test));
|
|
|
|
|
seriesContents.AddRange(GetSeriesContent(_jellyfin, test));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var albumsContents = GetMusicContent(_lidarrAlbumRepository, test);
|
|
|
|
|
|
|
|
|
|
var body = await BuildHtml(moviesContents, seriesContents, albumsContents, settings);
|
|
|
|
|
|
|
|
|
|
if (body.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!test)
|
|
|
|
|
{
|
|
|
|
|
var users = new List<OmbiUser>();
|
|
|
|
|
foreach (var emails in settings.ExternalEmails)
|
|
|
|
|
{
|
|
|
|
|
users.Add(new OmbiUser
|
|
|
|
|
{
|
|
|
|
|
UserName = emails,
|
|
|
|
|
Email = emails
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the users to send it to
|
|
|
|
|
users.AddRange(await _userManager.GetUsersInRoleAsync(OmbiRoles.ReceivesNewsletter));
|
|
|
|
|
if (!users.Any())
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var messageContent = ParseTemplate(template, customization);
|
|
|
|
|
var email = new NewsletterTemplate();
|
|
|
|
|
|
|
|
|
|
foreach (var user in users.DistinctBy(x => x.Email))
|
|
|
|
|
{ // Get the users to send it to
|
|
|
|
|
if (user.Email.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var url = GenerateUnsubscribeLink(customization.ApplicationUrl, user.Id);
|
|
|
|
|
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo, url);
|
|
|
|
|
|
|
|
|
|
var bodyBuilder = new BodyBuilder
|
|
|
|
|
{
|
|
|
|
|
HtmlBody = html,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var message = new MimeMessage
|
|
|
|
|
{
|
|
|
|
|
Body = bodyBuilder.ToMessageBody(),
|
|
|
|
|
Subject = messageContent.Subject
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Send the message to the user
|
|
|
|
|
message.To.Add(new MailboxAddress(user.Email.Trim(), user.Email.Trim()));
|
|
|
|
|
|
|
|
|
|
// Send the email
|
|
|
|
|
await _email.Send(message, emailSettings);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now add all of this to the Recently Added log
|
|
|
|
|
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
|
|
|
|
|
AddToRecentlyAddedLog(moviesContents, recentlyAddedLog);
|
|
|
|
|
AddToRecentlyAddedLog(seriesContents, recentlyAddedLog);
|
|
|
|
|
await _recentlyAddedLog.AddRange(recentlyAddedLog);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var admins = await _userManager.GetUsersInRoleAsync(OmbiRoles.Admin);
|
|
|
|
|
foreach (var a in admins)
|
|
|
|
|
{
|
|
|
|
|
if (a.Email.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var unsubscribeLink = GenerateUnsubscribeLink(customization.ApplicationUrl, a.Id);
|
|
|
|
|
|
|
|
|
|
var messageContent = ParseTemplate(template, customization);
|
|
|
|
|
|
|
|
|
|
var email = new NewsletterTemplate();
|
|
|
|
|
|
|
|
|
|
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo, unsubscribeLink);
|
|
|
|
|
|
|
|
|
|
await _email.Send(
|
|
|
|
|
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email },
|
|
|
|
|
emailSettings);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
await _notification.SendNotificationToAdmins( "Newsletter Failed");
|
|
|
|
|
_log.LogError(e, "Error when attempting to create newsletter");
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await _notification.SendNotificationToAdmins("Newsletter Finished");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AddToRecentlyAddedLog(ICollection<IMediaServerContent> moviesContents,
|
|
|
|
|
HashSet<RecentlyAddedLog> recentlyAddedLog)
|
|
|
|
|
{
|
|
|
|
|
foreach (var p in moviesContents)
|
|
|
|
|
{
|
|
|
|
|
recentlyAddedLog.Add(new RecentlyAddedLog
|
|
|
|
|
{
|
|
|
|
|
AddedAt = DateTime.Now,
|
|
|
|
|
Type = p.RecentlyAddedType,
|
|
|
|
|
ContentType = ContentType.Parent,
|
|
|
|
|
ContentId = StringHelper.IntParseLinq(p.TheMovieDbId),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private void AddToRecentlyAddedLog(ICollection<IMediaServerEpisode> episodes,
|
|
|
|
|
HashSet<RecentlyAddedLog> recentlyAddedLog)
|
|
|
|
|
{
|
|
|
|
|
foreach (var p in episodes)
|
|
|
|
|
{
|
|
|
|
|
recentlyAddedLog.Add(new RecentlyAddedLog
|
|
|
|
|
{
|
|
|
|
|
AddedAt = DateTime.Now,
|
|
|
|
|
Type = p.Series.RecentlyAddedType,
|
|
|
|
|
ContentType = ContentType.Episode,
|
|
|
|
|
ContentId = StringHelper.IntParseLinq(p.Series.TvDbId),
|
|
|
|
|
EpisodeNumber = p.EpisodeNumber,
|
|
|
|
|
SeasonNumber = p.SeasonNumber
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void GetRecentlyAddedMoviesData(List<RecentlyAddedLog> addedLog, out HashSet<string> addedAlbumLogIds)
|
|
|
|
|
{
|
|
|
|
|
var lidarrParent = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album);
|
|
|
|
|
addedAlbumLogIds = lidarrParent != null && lidarrParent.Any() ? (lidarrParent?.Select(x => x.AlbumId)?.ToHashSet() ?? new HashSet<string>()) : new HashSet<string>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<HashSet<IMediaServerContent>> GetMoviesContent<T>(IMediaServerContentRepository<T> repository, bool test) where T : class, IMediaServerContent
|
|
|
|
|
{
|
|
|
|
|
IQueryable<IMediaServerContent> content = repository.GetAll().Include(x => x.Episodes).AsNoTracking().Where(x => x.Type == MediaType.Movie).OrderByDescending(x => x.AddedAt);
|
|
|
|
|
var localDataset = content.Where(x => x.Type == MediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet();
|
|
|
|
|
|
|
|
|
|
HashSet<IMediaServerContent> moviesToSend;
|
|
|
|
|
if (test)
|
|
|
|
|
{
|
|
|
|
|
moviesToSend = content.Take(10).ToHashSet();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Filter out the ones that we haven't sent yet
|
|
|
|
|
var parent = _recentlyAddedLog.GetAll().Where(x => x.Type == repository.RecentlyAddedType
|
|
|
|
|
&& x.ContentType == ContentType.Parent).ToList();
|
|
|
|
|
var addedMovieLogIds = parent != null && parent.Any() ? (parent?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet<int>()) : new HashSet<int>();
|
|
|
|
|
moviesToSend = localDataset.Where(x => !addedMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))).ToHashSet();
|
|
|
|
|
_log.LogInformation("Movies to send: {0}", moviesToSend.Count());
|
|
|
|
|
|
|
|
|
|
// Find the movies that do not yet have MovieDbIds
|
|
|
|
|
var needsMovieDb = content.Where(x => x.Type == MediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet();
|
|
|
|
|
var newMovies = await GetMoviesWithoutId(addedMovieLogIds, needsMovieDb, repository);
|
|
|
|
|
moviesToSend = moviesToSend.Union(newMovies).ToHashSet();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_log.LogInformation("Movies to send: {0}", moviesToSend.Count());
|
|
|
|
|
return moviesToSend.DistinctBy(x => x.Id).ToHashSet();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private HashSet<IMediaServerEpisode> GetSeriesContent<T>(IMediaServerContentRepository<T> repository, bool test) where T : class, IMediaServerContent
|
|
|
|
|
{
|
|
|
|
|
var content = repository.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).AsNoTracking();
|
|
|
|
|
|
|
|
|
|
HashSet<IMediaServerEpisode> episodesToSend;
|
|
|
|
|
if (test)
|
|
|
|
|
{
|
|
|
|
|
var count = repository.GetAllEpisodes().Count();
|
|
|
|
|
episodesToSend = content.Take(10).ToHashSet();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Filter out the ones that we haven't sent yet
|
|
|
|
|
var addedEpisodesLogIds =
|
|
|
|
|
_recentlyAddedLog.GetAll().Where(x => x.Type == repository.RecentlyAddedType && x.ContentType == ContentType.Episode);
|
|
|
|
|
episodesToSend =
|
|
|
|
|
FilterEpisodes(content, addedEpisodesLogIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_log.LogInformation("Episodes to send: {0}", episodesToSend.Count());
|
|
|
|
|
return episodesToSend;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
private HashSet<LidarrAlbumCache> GetMusicContent(IExternalRepository<LidarrAlbumCache> repository, bool test)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
var lidarrContent = repository.GetAll().AsNoTracking().ToList().Where(x => x.FullyAvailable);
|
|
|
|
|
|
|
|
|
|
HashSet<LidarrAlbumCache> albumsToSend;
|
|
|
|
|
if (test)
|
|
|
|
|
{
|
|
|
|
|
albumsToSend = lidarrContent.OrderByDescending(x => x.AddedAt).Take(10).ToHashSet();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Filter out the ones that we haven't sent yet
|
|
|
|
|
var addedLog = _recentlyAddedLog.GetAll().ToList();
|
|
|
|
|
HashSet<string> addedAlbumLogIds;
|
|
|
|
|
GetRecentlyAddedMoviesData(addedLog, out addedAlbumLogIds);
|
|
|
|
|
albumsToSend = lidarrContent.Where(x => !addedAlbumLogIds.Contains(x.ForeignAlbumId)).ToHashSet();
|
|
|
|
|
}
|
|
|
|
|
_log.LogInformation("Albums to send: {0}", albumsToSend.Count());
|
|
|
|
|
return albumsToSend;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string GenerateUnsubscribeLink(string applicationUrl, string id)
|
|
|
|
|
{
|
|
|
|
|
if (!applicationUrl.HasValue() || !id.HasValue())
|
|
|
|
|
{
|
|
|
|
|
return string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!applicationUrl.EndsWith('/'))
|
|
|
|
|
{
|
|
|
|
|
applicationUrl += '/';
|
|
|
|
|
}
|
|
|
|
|
var b = new UriBuilder($"{applicationUrl}unsubscribe/{id}");
|
|
|
|
|
return b.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<HashSet<IMediaServerContent>> GetMoviesWithoutId<T>(HashSet<int> addedMovieLogIds, HashSet<IMediaServerContent> needsMovieDb, IMediaServerContentRepository<T> repository) where T : class, IMediaServerContent
|
|
|
|
|
{
|
|
|
|
|
foreach (var movie in needsMovieDb)
|
|
|
|
|
{
|
|
|
|
|
var id = await _refreshMetadata.GetTheMovieDbId(false, true, null, movie.ImdbId, movie.Title, true);
|
|
|
|
|
movie.TheMovieDbId = id.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = needsMovieDb.Where(x => x.HasTheMovieDb && !addedMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId)));
|
|
|
|
|
await UpdateTheMovieDbId(result, repository);
|
|
|
|
|
// Filter them out now
|
|
|
|
|
return result.ToHashSet();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task UpdateTheMovieDbId<T>(IEnumerable<IMediaServerContent> content, IMediaServerContentRepository<T> repository) where T : class, IMediaServerContent
|
|
|
|
|
{
|
|
|
|
|
foreach (var movie in content)
|
|
|
|
|
{
|
|
|
|
|
if (!movie.HasTheMovieDb)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
var entity = await repository.Find(movie.Id);
|
|
|
|
|
if (entity == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
entity.TheMovieDbId = movie.TheMovieDbId;
|
|
|
|
|
repository.UpdateWithoutSave(entity);
|
|
|
|
|
}
|
|
|
|
|
await repository.SaveChangesAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task Execute(IJobExecutionContext job)
|
|
|
|
|
{
|
|
|
|
|
var newsletterSettings = await _newsletterSettings.GetSettingsAsync();
|
|
|
|
|
await Start(newsletterSettings, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private HashSet<IMediaServerEpisode> FilterEpisodes(IEnumerable<IMediaServerEpisode> source, IEnumerable<RecentlyAddedLog> recentlyAdded)
|
|
|
|
|
{
|
|
|
|
|
var itemsToReturn = new HashSet<IMediaServerEpisode>();
|
|
|
|
|
foreach (var ep in source.Where(x => x.Series.HasTvDb // needed for recentlyAddedLog
|
|
|
|
|
&& x.Series.HasTheMovieDb // needed to fetch info to publish, this is just in case...
|
|
|
|
|
))
|
|
|
|
|
{
|
|
|
|
|
var tvDbId = StringHelper.IntParseLinq(ep.Series.TvDbId);
|
|
|
|
|
if (recentlyAdded.Any(x => x.ContentId == tvDbId && x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == ep.SeasonNumber))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
itemsToReturn.Add(ep);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return itemsToReturn;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings)
|
|
|
|
|
{
|
|
|
|
|
var resolver = new NotificationMessageResolver();
|
|
|
|
|
var curlys = new NotificationMessageCurlys();
|
|
|
|
|
|
|
|
|
|
curlys.SetupNewsletter(settings);
|
|
|
|
|
|
|
|
|
|
return resolver.ParseMessage(template, curlys);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<string> BuildHtml(ICollection<IMediaServerContent> movies,
|
|
|
|
|
IEnumerable<IMediaServerEpisode> episodes, HashSet<LidarrAlbumCache> albums, NewsletterSettings settings)
|
|
|
|
|
{
|
|
|
|
|
var ombiSettings = await _ombiSettings.GetSettingsAsync();
|
|
|
|
|
sb = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
if (movies.Any() && !settings.DisableMovies)
|
|
|
|
|
{
|
|
|
|
|
sb.Append($"<h1 style=\"text-align: center; max-width: 1042px;\">{Texts.NewMovies}</h1><br /><br />");
|
|
|
|
|
sb.Append(
|
|
|
|
|
"<table class=\"movies-table\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
|
|
|
|
|
sb.Append("<tr>");
|
|
|
|
|
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
|
|
|
|
|
sb.Append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
|
|
|
|
|
sb.Append("<tr>");
|
|
|
|
|
await ProcessMovies(movies, ombiSettings.DefaultLanguageCode);
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("</table>");
|
|
|
|
|
sb.Append("</td>");
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("</table>");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (episodes.Any() && !settings.DisableTv)
|
|
|
|
|
{
|
|
|
|
|
sb.Append($"<br /><br /><h1 style=\"text-align: center; max-width: 1042px;\">{Texts.NewTV}</h1><br /><br />");
|
|
|
|
|
sb.Append(
|
|
|
|
|
"<table class=\"tv-table\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
|
|
|
|
|
sb.Append("<tr>");
|
|
|
|
|
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
|
|
|
|
|
sb.Append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
|
|
|
|
|
sb.Append("<tr>");
|
|
|
|
|
await ProcessTv(episodes, ombiSettings.DefaultLanguageCode);
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("</table>");
|
|
|
|
|
sb.Append("</td>");
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("</table>");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (albums.Any() && !settings.DisableMusic)
|
|
|
|
|
{
|
|
|
|
|
sb.Append($"<h1 style=\"text-align: center; max-width: 1042px;\">{Texts.NewAlbums}</h1><br /><br />");
|
|
|
|
|
sb.Append(
|
|
|
|
|
"<table class=\"movies-table\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
|
|
|
|
|
sb.Append("<tr>");
|
|
|
|
|
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
|
|
|
|
|
sb.Append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
|
|
|
|
|
sb.Append("<tr>");
|
|
|
|
|
await ProcessAlbums(albums);
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("</table>");
|
|
|
|
|
sb.Append("</td>");
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("</table>");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ProcessMovies(ICollection<IMediaServerContent> plexContentToSend, string defaultLanguageCode)
|
|
|
|
|
{
|
|
|
|
|
int count = 0;
|
|
|
|
|
var ordered = plexContentToSend;
|
|
|
|
|
foreach (var content in ordered)
|
|
|
|
|
{
|
|
|
|
|
int.TryParse(content.TheMovieDbId, out var movieDbId);
|
|
|
|
|
var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId, defaultLanguageCode);
|
|
|
|
|
var mediaurl = content.Url;
|
|
|
|
|
if (info == null)
|
|
|
|
|
{
|
|
|
|
|
_log.LogError($"TMDB does not know movie {content.Title}. This shouldn't happen because our media server knows it as ID '{movieDbId}'.");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
CreateMovieHtmlContent(info, mediaurl);
|
|
|
|
|
count += 1;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
_log.LogError(e, "Error when Processing Movies {0}", info.Title);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
EndLoopHtml();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count == 2)
|
|
|
|
|
{
|
|
|
|
|
count = 0;
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("<tr>");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private async Task ProcessAlbums(HashSet<LidarrAlbumCache> albumsToSend)
|
|
|
|
|
{
|
|
|
|
|
var settings = await _lidarrSettings.GetSettingsAsync();
|
|
|
|
|
int count = 0;
|
|
|
|
|
var ordered = albumsToSend.OrderByDescending(x => x.AddedAt);
|
|
|
|
|
foreach (var content in ordered)
|
|
|
|
|
{
|
|
|
|
|
var info = await _lidarrApi.GetAlbumByForeignId(content.ForeignAlbumId, settings.ApiKey, settings.FullUri);
|
|
|
|
|
if (info == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
CreateAlbumHtmlContent(info);
|
|
|
|
|
count += 1;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
_log.LogError(e, "Error when Processing Lidarr Album {0}", info.title);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
EndLoopHtml();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count == 2)
|
|
|
|
|
{
|
|
|
|
|
count = 0;
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("<tr>");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CreateMovieHtmlContent(MovieResponseDto info, string mediaurl)
|
|
|
|
|
{
|
|
|
|
|
AddBackgroundInsideTable($"https://image.tmdb.org/t/p/w1280/{info.BackdropPath}");
|
|
|
|
|
AddPosterInsideTable($"https://image.tmdb.org/t/p/original{info.PosterPath}");
|
|
|
|
|
|
|
|
|
|
AddMediaServerUrl(mediaurl, $"https://image.tmdb.org/t/p/original{info.PosterPath}");
|
|
|
|
|
AddInfoTable();
|
|
|
|
|
|
|
|
|
|
var releaseDate = string.Empty;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
releaseDate = $"({DateTime.Parse(info.ReleaseDate).Year})";
|
|
|
|
|
}
|
|
|
|
|
#pragma warning disable RCS1075 // Avoid empty catch clause that catches System.Exception.
|
|
|
|
|
catch (Exception)
|
|
|
|
|
#pragma warning restore RCS1075 // Avoid empty catch clause that catches System.Exception.
|
|
|
|
|
{
|
|
|
|
|
// Swallow, couldn't parse the date
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AddTitle($"https://www.imdb.com/title/{info.ImdbId}/", $"{info.Title} {releaseDate}");
|
|
|
|
|
|
|
|
|
|
var summary = info.Overview;
|
|
|
|
|
if (summary.Length > 280)
|
|
|
|
|
{
|
|
|
|
|
summary = summary.Remove(280);
|
|
|
|
|
summary = summary + "...</p>";
|
|
|
|
|
}
|
|
|
|
|
AddParagraph(summary);
|
|
|
|
|
|
|
|
|
|
if (info.Genres.Any())
|
|
|
|
|
{
|
|
|
|
|
AddGenres($"{Texts.GenresLabel} {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CreateAlbumHtmlContent(AlbumLookup info)
|
|
|
|
|
{
|
|
|
|
|
var cover = info.images
|
|
|
|
|
.FirstOrDefault(x => x.coverType.Equals("cover", StringComparison.InvariantCultureIgnoreCase))?.url;
|
|
|
|
|
if (cover.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
cover = info.remoteCover;
|
|
|
|
|
}
|
|
|
|
|
AddBackgroundInsideTable(cover);
|
|
|
|
|
var disk = info.images
|
|
|
|
|
.FirstOrDefault(x => x.coverType.Equals("disc", StringComparison.InvariantCultureIgnoreCase))?.url;
|
|
|
|
|
if (disk.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
disk = info.remoteCover;
|
|
|
|
|
}
|
|
|
|
|
AddPosterInsideTable(disk);
|
|
|
|
|
|
|
|
|
|
AddMediaServerUrl(string.Empty, string.Empty);
|
|
|
|
|
AddInfoTable();
|
|
|
|
|
|
|
|
|
|
var releaseDate = $"({info.releaseDate.Year})";
|
|
|
|
|
|
|
|
|
|
AddTitle(string.Empty, $"{info.title} {releaseDate}");
|
|
|
|
|
|
|
|
|
|
var summary = info.artist?.artistName ?? string.Empty;
|
|
|
|
|
if (summary.Length > 280)
|
|
|
|
|
{
|
|
|
|
|
summary = summary.Remove(280);
|
|
|
|
|
summary = summary + "...</p>";
|
|
|
|
|
}
|
|
|
|
|
AddParagraph(summary);
|
|
|
|
|
|
|
|
|
|
AddGenres($"{Texts.AlbumTypeLabel} {info.albumType}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ProcessTv(IEnumerable<IMediaServerEpisode> episodes, string languageCode)
|
|
|
|
|
{
|
|
|
|
|
var series = new List<IMediaServerContent>();
|
|
|
|
|
foreach (var episode in episodes)
|
|
|
|
|
{
|
|
|
|
|
var existingSeries = episode.SeriesIsIn(series);
|
|
|
|
|
if (existingSeries != null)
|
|
|
|
|
{
|
|
|
|
|
if (!episode.IsIn(existingSeries))
|
|
|
|
|
{
|
|
|
|
|
existingSeries.Episodes.Add(episode);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
episode.Series.Episodes = new List<IMediaServerEpisode> { episode };
|
|
|
|
|
series.Add(episode.Series);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
var orderedTv = series.OrderByDescending(x => x.AddedAt);
|
|
|
|
|
foreach (var t in orderedTv)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId, languageCode);
|
|
|
|
|
if (tvInfo == null)
|
|
|
|
|
{
|
|
|
|
|
_log.LogError($"TMDB does not know series {t.Title}. This shouldn't happen because our media server knows it as ID '{t.TheMovieDbId}'.");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (tvInfo.backdrop_path.HasValue())
|
|
|
|
|
{
|
|
|
|
|
AddBackgroundInsideTable($"https://image.tmdb.org/t/p/w500{tvInfo.backdrop_path}");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
AddBackgroundInsideTable($"https://image.tmdb.org/t/p/w1280/");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var banner = tvInfo.poster_path;
|
|
|
|
|
if (!string.IsNullOrEmpty(banner))
|
|
|
|
|
{
|
|
|
|
|
banner = $"https://image.tmdb.org/t/p/w300/{banner?.TrimStart('/') ?? string.Empty}";
|
|
|
|
|
};
|
|
|
|
|
AddPosterInsideTable(banner);
|
|
|
|
|
AddMediaServerUrl(t.Url, banner);
|
|
|
|
|
|
|
|
|
|
AddInfoTable();
|
|
|
|
|
|
|
|
|
|
AddTvTitle(tvInfo);
|
|
|
|
|
|
|
|
|
|
var tvEpisodesString = GetTvEpisodesString(tvInfo, t.Episodes);
|
|
|
|
|
AddTvEpisodesSummaryGenres(tvEpisodesString, tvInfo);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
_log.LogError(e, "Error when processing TV {0}", t.Title);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
EndLoopHtml();
|
|
|
|
|
count += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count == 2)
|
|
|
|
|
{
|
|
|
|
|
count = 0;
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("<tr>");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GetTvEpisodesString(TvInfo tvInfo, ICollection<IMediaServerEpisode> episodes)
|
|
|
|
|
{
|
|
|
|
|
if (episodes.Count >= tvInfo.number_of_episodes)
|
|
|
|
|
{
|
|
|
|
|
// do not list individual episodes when the series is complete
|
|
|
|
|
return string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
// Group by the season number
|
|
|
|
|
var seasons = episodes.GroupBy(p => p.SeasonNumber,
|
|
|
|
|
(key, g) => new
|
|
|
|
|
{
|
|
|
|
|
SeasonNumber = key,
|
|
|
|
|
Episodes = g.ToList(),
|
|
|
|
|
Header = tvInfo?.seasons?.Where(x => x.season_number == key).FirstOrDefault(),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
// Group the episodes
|
|
|
|
|
foreach (var season in seasons.OrderBy(x => x.SeasonNumber))
|
|
|
|
|
{
|
|
|
|
|
string episodeList;
|
|
|
|
|
if (season.Episodes.Count >= season.Header.episode_count)
|
|
|
|
|
{
|
|
|
|
|
// do not list individual episodes when the season is complete
|
|
|
|
|
episodeList = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var orderedEpisodes = season.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
|
|
|
|
|
episodeList = $"{Texts.EpisodesLabel} {StringHelper.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber))}";
|
|
|
|
|
}
|
|
|
|
|
var episodeAirDate = season.Header.air_date;
|
|
|
|
|
sb.Append($"{Texts.SeasonLabel} {season.SeasonNumber} - {episodeList} {episodeAirDate}");
|
|
|
|
|
sb.Append("<br />");
|
|
|
|
|
}
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
}
|
|
|
|
|
private void AddTvTitle(TvInfo tvInfo)
|
|
|
|
|
{
|
|
|
|
|
var title = "";
|
|
|
|
|
if (!String.IsNullOrEmpty(tvInfo.first_air_date) && tvInfo.first_air_date.Length > 4)
|
|
|
|
|
{
|
|
|
|
|
title = $"{tvInfo.name} ({tvInfo.first_air_date.Remove(4)})";
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
title = $"{tvInfo.name}";
|
|
|
|
|
}
|
|
|
|
|
AddTitle($"https://www.themoviedb.org/tv/{tvInfo.id}/", title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AddTvEpisodesSummaryGenres(string episodes, TvInfo tvInfo)
|
|
|
|
|
{
|
|
|
|
|
var summary = tvInfo.overview;
|
|
|
|
|
if (summary.Length > 280)
|
|
|
|
|
{
|
|
|
|
|
summary = summary.Remove(280);
|
|
|
|
|
summary = summary + "...</p>";
|
|
|
|
|
}
|
|
|
|
|
AddTvParagraph(episodes, summary);
|
|
|
|
|
|
|
|
|
|
if (tvInfo.genres.Any())
|
|
|
|
|
{
|
|
|
|
|
AddGenres($"{Texts.GenresLabel} {string.Join(", ", tvInfo.genres.Select(x => x.name.ToString()).ToArray())}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EndLoopHtml()
|
|
|
|
|
{
|
|
|
|
|
//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("</table>");
|
|
|
|
|
sb.Append("</td>");
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("</table>");
|
|
|
|
|
sb.Append("</td>");
|
|
|
|
|
sb.Append("</tr>");
|
|
|
|
|
sb.Append("</table>");
|
|
|
|
|
sb.Append("</td>");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.Port.ToString()))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool _disposed;
|
|
|
|
|
|
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (_disposed)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (disposing)
|
|
|
|
|
{
|
|
|
|
|
//_newsletterSettings?.Dispose();
|
|
|
|
|
//_customizationSettings?.Dispose();
|
|
|
|
|
//_emailSettings.Dispose();
|
|
|
|
|
_templateRepo?.Dispose();
|
|
|
|
|
_userManager?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
_disposed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
Dispose(true);
|
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|