You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Ombi/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs

842 lines
34 KiB

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, customization.ApplicationUrl ?? string.Empty);
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, customization.ApplicationUrl ?? string.Empty);
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);
}
}
}