diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ac6457d..d1efdccb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +## (unreleased) + +### **New Features** + +- Added a check for long movie descriptions and dealt with accordingly. [Anojh] + +- Update jobs.component.html. [D34DC3N73R] + +- Added id to emby button to distinguish for UI purposes. [Anojh] + +- Changed theme content textarea to use monospace font. [Anojh] + +- Added classes and ids to issue status. [Anojh] + +- Changed overlay picture to poster pic so we have fallback styling on older clients. [Anojh] + +### **Fixes** + +- Fixed #2224. [Jamie] + +- More robust check for release date. [Anojh] + +- Fixed duplicate titles in Plex Newsletter. [Anojh] + +- Fixed the filter on the Requests page #2219 and added the default sort to be most recent requests. [Jamie Rees] + +- Enable the mobile ntoifications inside Ombi. [Jamie Rees] + +- Made the episode list in the newsletter easier to read. Rather than 1,2,3,4,5,10 we will now show 1-5, 10. [Jamie Rees] + +- Moved the RecentlyAddedSync into it's own job, it still is calls the regular sync but this should make it easier to start the job from the UI (When I add that) [Jamie Rees] + +- Made a massive improvement on the Smaller more frequent Plex Job. This should pick up content a lot quicker now and also get their metadata a lot quicker. [Jamie Rees] + +- Trigger a metadata refresh when we finish scanning the libraries. [Jamie Rees] + +- Fixed a potential issue in the newsletter where it wouldn't send content due to missing metadata, but would mark it as if it was sent. [Jamie Rees] + +- Fixed settings retaining active class when elsewhere in UI. [Anojh] + +- Separated user and subject details into spans and fixed styling. [Anojh] + +- Fixed linting errors. [Anojh] + +- Fixed settings nav item not retaining active class when in other tabs in the settings page. [Anojh] + +- Separated reported by and subject and added classes. [Anojh] + +- Fix for issue #2152. [Anojh] + +- Fix genres being ambigious error. [Anojh] + +- Made text style justified. [Anojh] + +- V1.0, needs TV background and needs styles for outlook. [Anojh] + +- CSS done for the template. [Anojh] + +- Fixing some format issues. [Anojh] + +- Newsletter template structure done. [Anojh] + + ## v3.0.3268 (2018-04-28) ### **Fixes** diff --git a/src/Ombi.Api.Plex/Models/Metadata.cs b/src/Ombi.Api.Plex/Models/Metadata.cs index dcde62818..cda6ce68b 100644 --- a/src/Ombi.Api.Plex/Models/Metadata.cs +++ b/src/Ombi.Api.Plex/Models/Metadata.cs @@ -11,15 +11,15 @@ namespace Ombi.Api.Plex.Models public string summary { get; set; } public int index { get; set; } public float rating { get; set; } - public int viewCount { get; set; } - public int lastViewedAt { get; set; } + //public int viewCount { get; set; } + //public int lastViewedAt { get; set; } public int year { get; set; } public string thumb { get; set; } public string art { get; set; } public string banner { get; set; } public string theme { get; set; } - public string duration { get; set; } - public string originallyAvailableAt { get; set; } + //public string duration { get; set; } + //public string originallyAvailableAt { get; set; } public int leafCount { get; set; } public int viewedLeafCount { get; set; } public int childCount { get; set; } diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs b/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs index 2f765fede..08ec9b594 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs @@ -17,7 +17,7 @@ namespace Ombi.Core.Engine.Interfaces Task ApproveMovie(MovieRequests request); Task ApproveMovieById(int requestId); Task DenyMovieById(int modelId); - Task> Filter(FilterViewModel vm); + Task> Filter(FilterViewModel vm); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 0e47a28f6..3d88b7230 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -131,11 +131,11 @@ namespace Ombi.Core.Engine List allRequests; if (shouldHide.Hide) { - allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).Skip(position).Take(count).ToListAsync(); + allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).Skip(position).Take(count).OrderByDescending(x => x.ReleaseDate).ToListAsync(); } else { - allRequests = await MovieRepository.GetWithUser().Skip(position).Take(count).ToListAsync(); + allRequests = await MovieRepository.GetWithUser().Skip(position).Take(count).OrderByDescending(x => x.ReleaseDate).ToListAsync(); } allRequests.ForEach(x => { @@ -380,10 +380,13 @@ namespace Ombi.Core.Engine return new RequestEngineResult { Result = true, Message = $"{movieName} has been successfully added!" }; } - public async Task> Filter(FilterViewModel vm) + public async Task> Filter(FilterViewModel vm) { var shouldHide = await HideFromOtherUsers(); - var requests = shouldHide.Hide ? MovieRepository.GetWithUser(shouldHide.UserId) : MovieRepository.GetWithUser(); + var requests = shouldHide.Hide + ? MovieRepository.GetWithUser(shouldHide.UserId) + : MovieRepository.GetWithUser(); + switch (vm.AvailabilityFilter) { case FilterType.None: @@ -415,7 +418,14 @@ namespace Ombi.Core.Engine throw new ArgumentOutOfRangeException(); } - return requests; + var count = await requests.CountAsync(); + requests = requests.Skip(vm.Position).Take(vm.Count); + var retVal = new FilterResult + { + Total = count, + Collection = requests + }; + return retVal; } } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index c3f1139bf..2ce4da292 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -141,7 +141,7 @@ namespace Ombi.Core.Engine .Include(x => x.ChildRequests) .ThenInclude(x => x.SeasonRequests) .ThenInclude(x => x.Episodes) - .Skip(position).Take(count).ToListAsync(); + .Skip(position).Take(count).OrderByDescending(x => x.ReleaseDate).ToListAsync(); // Filter out children @@ -153,7 +153,7 @@ namespace Ombi.Core.Engine .Include(x => x.ChildRequests) .ThenInclude(x => x.SeasonRequests) .ThenInclude(x => x.Episodes) - .Skip(position).Take(count).ToListAsync(); + .Skip(position).Take(count).OrderByDescending(x => x.ReleaseDate).ToListAsync(); } return allRequests; diff --git a/src/Ombi.Core/Models/Requests/FilterResult.cs b/src/Ombi.Core/Models/Requests/FilterResult.cs new file mode 100644 index 000000000..467b9731a --- /dev/null +++ b/src/Ombi.Core/Models/Requests/FilterResult.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Ombi.Core.Models.Requests +{ + public class FilterResult + { + public int Total { get; set; } + public IEnumerable Collection { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/Requests/FilterViewModel.cs b/src/Ombi.Core/Models/Requests/FilterViewModel.cs index 27ccc39d6..25a9d5ef1 100644 --- a/src/Ombi.Core/Models/Requests/FilterViewModel.cs +++ b/src/Ombi.Core/Models/Requests/FilterViewModel.cs @@ -4,6 +4,8 @@ { public FilterType AvailabilityFilter { get; set; } public FilterType StatusFilter { get; set; } + public int Position { get; set; } + public int Count { get; set; } } public enum FilterType diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index c5a365bf6..68f4b7218 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -177,6 +177,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } } } diff --git a/src/Ombi.Mapping/Profiles/MovieProfile.cs b/src/Ombi.Mapping/Profiles/MovieProfile.cs index dd7e03379..39961b785 100644 --- a/src/Ombi.Mapping/Profiles/MovieProfile.cs +++ b/src/Ombi.Mapping/Profiles/MovieProfile.cs @@ -68,7 +68,7 @@ namespace Ombi.Mapping.Profiles .ForMember(x => x.ReleaseDate, o => o.MapFrom(s => s.release_date)) .ForMember(x => x.Type, o => o.MapFrom(s => s.Type)); - CreateMap(); + CreateMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); diff --git a/src/Ombi.Notifications.Templates/TemplateBase.cs b/src/Ombi.Notifications.Templates/TemplateBase.cs index 9b9207c9a..ccabfa65c 100644 --- a/src/Ombi.Notifications.Templates/TemplateBase.cs +++ b/src/Ombi.Notifications.Templates/TemplateBase.cs @@ -3,6 +3,6 @@ public abstract class TemplateBase { public abstract string TemplateLocation { get; } - public virtual string OmbiLogo => "http://i.imgur.com/qQsN78U.png"; + public virtual string OmbiLogo => "http://i.imgur.com/7pqVq7W.png"; } } \ No newline at end of file diff --git a/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html index 464e3463b..e9a2b7f29 100644 --- a/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html +++ b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html @@ -4,57 +4,125 @@ Ombi - - - + +
- - + +
  -
+
+
- +
@@ -144,23 +165,24 @@
- +


-

{@INTRO}

+

{@INTRO}

- {@RECENTLYADDED} -
+ {@RECENTLYADDED}
diff --git a/src/Ombi.Notifications/Agents/MobileNotification.cs b/src/Ombi.Notifications/Agents/MobileNotification.cs index 05c02dfef..575801bd2 100644 --- a/src/Ombi.Notifications/Agents/MobileNotification.cs +++ b/src/Ombi.Notifications/Agents/MobileNotification.cs @@ -39,7 +39,7 @@ namespace Ombi.Notifications.Agents protected override bool ValidateConfiguration(MobileNotificationSettings settings) { - return false; + return true; } protected override async Task NewRequest(NotificationOptions model, MobileNotificationSettings settings) @@ -211,7 +211,7 @@ namespace Ombi.Notifications.Agents protected async Task Send(List playerIds, NotificationMessage model, MobileNotificationSettings settings) { - if (!playerIds.Any()) + if (playerIds == null || !playerIds.Any()) { return; } diff --git a/src/Ombi.Schedule.Tests/NewsletterTests.cs b/src/Ombi.Schedule.Tests/NewsletterTests.cs new file mode 100644 index 000000000..fcbd35107 --- /dev/null +++ b/src/Ombi.Schedule.Tests/NewsletterTests.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using Ombi.Core.Settings; +using Ombi.Schedule.Jobs.Ombi; +using Ombi.Settings.Settings.Models; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; + +namespace Ombi.Schedule.Tests +{ + [TestFixture] + public class NewsletterTests + { + [TestCaseSource(nameof(EpisodeListData))] + public string BuildEpisodeListTest(List episodes) + { + var emailSettings = new Mock>(); + var customziation = new Mock>(); + var newsletterSettings = new Mock>(); + var newsletter = new NewsletterJob(null, null, null, null, null, null, customziation.Object, emailSettings.Object, null, null, newsletterSettings.Object, null); + + var ep = new List(); + foreach (var i in episodes) + { + ep.Add(i); + } + var result = newsletter.BuildEpisodeList(ep); + return result; + } + + public static IEnumerable EpisodeListData + { + get + { + yield return new TestCaseData(new List{1,2,3,4,5,6}).Returns("1-6").SetName("Simple 1-6"); + yield return new TestCaseData(new List{1,2,3,4,5,6,8,9}).Returns("1-6, 8-9").SetName("Simple 1-6, 8-9"); + yield return new TestCaseData(new List{1,99,101,555,468,469}).Returns("1, 99, 101, 555, 468-469").SetName("More Complex"); + yield return new TestCaseData(new List{1}).Returns("1").SetName("Single Episode"); + } + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj index eae150067..1805b80f2 100644 --- a/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj +++ b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj @@ -8,10 +8,10 @@ - - - - + + + + diff --git a/src/Ombi.Schedule/JobSetup.cs b/src/Ombi.Schedule/JobSetup.cs index 73d7c3536..44997f272 100644 --- a/src/Ombi.Schedule/JobSetup.cs +++ b/src/Ombi.Schedule/JobSetup.cs @@ -19,7 +19,7 @@ namespace Ombi.Schedule IOmbiAutomaticUpdater updater, IEmbyContentSync embySync, IPlexUserImporter userImporter, IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache, ISettingsService jobsettings, ISickRageSync srSync, IRefreshMetadata refresh, - INewsletterJob newsletter) + INewsletterJob newsletter, IPlexRecentlyAddedSync recentlyAddedPlex) { _plexContentSync = plexContentSync; _radarrSync = radarrSync; @@ -33,9 +33,11 @@ namespace Ombi.Schedule _srSync = srSync; _refreshMetadata = refresh; _newsletter = newsletter; + _plexRecentlyAddedSync = recentlyAddedPlex; } private readonly IPlexContentSync _plexContentSync; + private readonly IPlexRecentlyAddedSync _plexRecentlyAddedSync; private readonly IRadarrSync _radarrSync; private readonly IOmbiAutomaticUpdater _updater; private readonly IPlexUserImporter _plexUserImporter; @@ -56,7 +58,7 @@ namespace Ombi.Schedule RecurringJob.AddOrUpdate(() => _sonarrSync.Start(), JobSettingsHelper.Sonarr(s)); RecurringJob.AddOrUpdate(() => _radarrSync.CacheContent(), JobSettingsHelper.Radarr(s)); RecurringJob.AddOrUpdate(() => _plexContentSync.CacheContent(false), JobSettingsHelper.PlexContent(s)); - RecurringJob.AddOrUpdate(() => _plexContentSync.CacheContent(true), JobSettingsHelper.PlexRecentlyAdded(s)); + RecurringJob.AddOrUpdate(() => _plexRecentlyAddedSync.Start(), JobSettingsHelper.PlexRecentlyAdded(s)); RecurringJob.AddOrUpdate(() => _cpCache.Start(), JobSettingsHelper.CouchPotato(s)); RecurringJob.AddOrUpdate(() => _srSync.Start(), JobSettingsHelper.SickRageSync(s)); RecurringJob.AddOrUpdate(() => _refreshMetadata.Start(), JobSettingsHelper.RefreshMetadata(s)); diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index 4d0026f48..acea1535e 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -9,6 +9,7 @@ using Ombi.Api.Emby.Models.Movie; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; using Ombi.Helpers; +using Ombi.Schedule.Jobs.Ombi; using Ombi.Store.Entities; using Ombi.Store.Repository; using Serilog; @@ -19,13 +20,14 @@ namespace Ombi.Schedule.Jobs.Emby public class EmbyContentSync : IEmbyContentSync { public EmbyContentSync(ISettingsService settings, IEmbyApi api, ILogger logger, - IEmbyContentRepository repo, IEmbyEpisodeSync epSync) + IEmbyContentRepository repo, IEmbyEpisodeSync epSync, IRefreshMetadata metadata) { _logger = logger; _settings = settings; _api = api; _repo = repo; _episodeSync = epSync; + _metadata = metadata; _settings.ClearCache(); } @@ -34,6 +36,7 @@ namespace Ombi.Schedule.Jobs.Emby private readonly IEmbyApi _api; private readonly IEmbyContentRepository _repo; private readonly IEmbyEpisodeSync _episodeSync; + private readonly IRefreshMetadata _metadata; public async Task Start() @@ -56,6 +59,7 @@ namespace Ombi.Schedule.Jobs.Emby // Episodes BackgroundJob.Enqueue(() => _episodeSync.Start()); + BackgroundJob.Enqueue(() => _metadata.Start()); } diff --git a/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs b/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs index 61f4bd7c8..3f9a182a4 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/HtmlTemplateGenerator.cs @@ -4,43 +4,79 @@ namespace Ombi.Schedule.Jobs.Ombi { public abstract class HtmlTemplateGenerator { - protected virtual void AddParagraph(StringBuilder stringBuilder, string text, int fontSize = 14, string fontWeight = "normal") + protected virtual void AddBackgroundInsideTable(StringBuilder sb, string url) { - stringBuilder.AppendFormat("

{0}

", text, fontSize, fontWeight); + sb.Append(""); + sb.AppendFormat("", url); + sb.Append(""); + sb.Append("
"); + sb.Append(""); } - protected virtual void AddImageInsideTable(StringBuilder sb, string url, int size = 400) + protected virtual void AddPosterInsideTable(StringBuilder sb, string url) { sb.Append(""); - sb.Append(""); + } + + protected virtual void AddInfoTable(StringBuilder sb) + { + sb.Append( + ""); + sb.Append(""); + } + } + } + + public string BuildEpisodeList(IEnumerable orderedEpisodes) + { + var epSb = new StringBuilder(); + var previousEpisodes = new List(); + var previousEpisode = -1; + foreach (var ep in orderedEpisodes) + { + if (ep - 1 == previousEpisode) + { + // This is the next one + previousEpisodes.Add(ep); + } + else + { + if (previousEpisodes.Count > 1) + { + // End it + epSb.Append($"{previousEpisodes.First()}-{previousEpisodes.Last()}, "); + } + else if (previousEpisodes.Count == 1) + { + epSb.Append($"{previousEpisodes.FirstOrDefault()}, "); + } + // New one + previousEpisodes.Clear(); + previousEpisodes.Add(ep); + } + previousEpisode = ep; + } + + if (previousEpisodes.Count > 1) + { + // Got some left over + epSb.Append($"{previousEpisodes.First()}-{previousEpisodes.Last()}"); + } + else if(previousEpisodes.Count == 1) + { + epSb.Append(previousEpisodes.FirstOrDefault()); + } + + return epSb.ToString(); } private async Task ProcessEmbyTv(HashSet embyContent, StringBuilder sb) @@ -570,9 +671,9 @@ namespace Ombi.Schedule.Jobs.Ombi series.Add(episode.Series); } } + + int count = 0; var orderedTv = series.OrderByDescending(x => x.AddedAt); - sb.Append( - "
"); - sb.Append($""); + sb.Append(""); + sb.AppendFormat("", url); + } + + protected virtual void AddMediaServerUrl(StringBuilder sb, string mediaurl, string url) + { + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append("
"); + sb.AppendFormat("", mediaurl); + sb.AppendFormat("", url); + sb.Append(""); + sb.Append("
"); + sb.Append("
"); + sb.Append(""); + } + + protected virtual void AddTitle(StringBuilder sb, string url, string title) + { + sb.Append(""); + sb.Append(""); sb.Append(""); } - protected virtual void Href(StringBuilder sb, string url) + protected virtual void AddParagraph(StringBuilder sb, string text) { - sb.AppendFormat("", url); + sb.Append(""); + sb.Append(""); + sb.Append(""); } - protected virtual void TableData(StringBuilder sb) + protected virtual void AddTvParagraph(StringBuilder sb, string episodes, string summary) { - sb.Append( - ""); + sb.Append(""); + sb.Append(""); } - protected virtual void EndTag(StringBuilder sb, string tag) + protected virtual void AddGenres(StringBuilder sb, string text) { - sb.AppendFormat("", tag); + sb.Append(""); + sb.Append(""); + sb.Append(""); } - - 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/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs index 8b246e97e..ed34a6236 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -98,14 +98,14 @@ namespace Ombi.Schedule.Jobs.Ombi addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Episode); // Filter out the ones that we haven't sent yet - var plexContentMoviesToSend = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !addedPlexMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))); - var embyContentMoviesToSend = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !addedEmbyMoviesLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))); + var plexContentMoviesToSend = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && x.HasTheMovieDb && !addedPlexMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))); + var embyContentMoviesToSend = embyContent.Where(x => x.Type == EmbyMediaType.Movie && x.HasTheMovieDb && !addedEmbyMoviesLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))); _log.LogInformation("Plex Movies to send: {0}", plexContentMoviesToSend.Count()); _log.LogInformation("Emby Movies to send: {0}", embyContentMoviesToSend.Count()); var plexEpisodesToSend = - FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), addedPlexEpisodesLogIds); - var embyEpisodesToSend = FilterEmbyEpisodes(_emby.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), + FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).Where(x => x.Series.HasTvDb).AsNoTracking(), addedPlexEpisodesLogIds); + var embyEpisodesToSend = FilterEmbyEpisodes(_emby.GetAllEpisodes().Include(x => x.Series).Where(x => x.Series.HasTvDb).AsNoTracking(), addedEmbyEpisodesLogIds); _log.LogInformation("Plex Episodes to send: {0}", plexEpisodesToSend.Count()); @@ -114,7 +114,7 @@ namespace Ombi.Schedule.Jobs.Ombi if (test) { var plexm = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie).OrderByDescending(x => x.AddedAt).Take(10); - var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10); + var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie ).OrderByDescending(x => x.AddedAt).Take(10); var plext = _plex.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).Take(10).ToHashSet(); var embyt = _emby.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10).ToHashSet(); body = await BuildHtml(plexm, embym, plext, embyt, settings); @@ -306,16 +306,38 @@ namespace Ombi.Schedule.Jobs.Ombi var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie); if ((plexMovies.Any() || embyMovies.Any()) && !settings.DisableMovies) { - sb.Append("

New Movies:



"); + sb.Append("

New Movies



"); + sb.Append( + "
"); + sb.AppendFormat("", url); + sb.AppendFormat("

{0}

", title); + sb.Append("
"); sb.Append("
"); + sb.AppendFormat("

{0}

", text); + sb.Append("
"); + sb.Append("
"); + sb.AppendFormat("

{0}

", episodes); + sb.AppendFormat("
{0}
", summary); + sb.Append("
"); + sb.AppendFormat("{0}", text); + sb.Append("
"); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append("
"); + sb.Append(""); + sb.Append(""); await ProcessPlexMovies(plexMovies, sb); await ProcessEmbyMovies(embyMovies, sb); + sb.Append(""); + sb.Append("
"); + sb.Append("
"); } if ((plexEpisodes.Any() || embyEp.Any()) && !settings.DisableTv) { - sb.Append("

New Episodes:



"); + sb.Append("

New TV



"); + sb.Append( + ""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append("
"); + sb.Append(""); + sb.Append(""); await ProcessPlexTv(plexEpisodes, sb); await ProcessEmbyTv(embyEp, sb); + sb.Append(""); + sb.Append("
"); + sb.Append("
"); } return sb.ToString(); @@ -323,8 +345,7 @@ namespace Ombi.Schedule.Jobs.Ombi private async Task ProcessPlexMovies(IQueryable plexContentToSend, StringBuilder sb) { - sb.Append( - ""); + int count = 0; var ordered = plexContentToSend.OrderByDescending(x => x.AddedAt); foreach (var content in ordered) { @@ -334,13 +355,15 @@ namespace Ombi.Schedule.Jobs.Ombi continue; } var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId); + var mediaurl = content.Url; if (info == null) { continue; } try { - CreateMovieHtmlContent(sb, info); + CreateMovieHtmlContent(sb, info, mediaurl); + count += 1; } catch (Exception e) { @@ -350,13 +373,19 @@ namespace Ombi.Schedule.Jobs.Ombi { EndLoopHtml(sb); } + + if (count == 2) + { + count = 0; + sb.Append(""); + sb.Append(""); + } } } private async Task ProcessEmbyMovies(IQueryable embyContent, StringBuilder sb) { - sb.Append( - "
"); + int count = 0; var ordered = embyContent.OrderByDescending(x => x.AddedAt); foreach (var content in ordered) { @@ -374,6 +403,7 @@ namespace Ombi.Schedule.Jobs.Ombi theMovieDbId = result.id.ToString(); } + var mediaurl = content.Url; var info = await _movieApi.GetMovieInformationWithExtraInfo(StringHelper.IntParseLinq(theMovieDbId)); if (info == null) { @@ -381,7 +411,8 @@ namespace Ombi.Schedule.Jobs.Ombi } try { - CreateMovieHtmlContent(sb, info); + CreateMovieHtmlContent(sb, info, mediaurl); + count += 1; } catch (Exception e) { @@ -391,17 +422,24 @@ namespace Ombi.Schedule.Jobs.Ombi { EndLoopHtml(sb); } + + if (count == 2) + { + count = 0; + sb.Append(""); + sb.Append(""); + } } } - private void CreateMovieHtmlContent(StringBuilder sb, MovieResponseDto info) + private void CreateMovieHtmlContent(StringBuilder sb, MovieResponseDto info, string mediaurl) { - AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.PosterPath}"); + AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/{info.BackdropPath}"); + AddPosterInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.PosterPath}"); - sb.Append(""); - TableData(sb); + AddMediaServerUrl(sb, mediaurl, $"https://image.tmdb.org/t/p/original{info.PosterPath}"); + AddInfoTable(sb); - Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/"); var releaseDate = string.Empty; try { @@ -411,16 +449,22 @@ namespace Ombi.Schedule.Jobs.Ombi { // Swallow, couldn't parse the date } - Header(sb, 3, $"{info.Title} {releaseDate}"); - EndTag(sb, "a"); + + AddTitle(sb, $"https://www.imdb.com/title/{info.ImdbId}/", $"{info.Title} {releaseDate}"); + + var summary = info.Overview; + if (summary.Length > 280) + { + summary = summary.Remove(280); + summary = summary + "...

"; + } + AddParagraph(sb, summary); if (info.Genres.Any()) { - AddParagraph(sb, - $"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}"); + AddGenres(sb, + $"Genres: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}"); } - - AddParagraph(sb, info.Overview); } private async Task ProcessPlexTv(HashSet plexContent, StringBuilder sb) @@ -444,9 +488,8 @@ namespace Ombi.Schedule.Jobs.Ombi } } + int count = 0; var orderedTv = series.OrderByDescending(x => x.AddedAt); - sb.Append( - "
"); foreach (var t in orderedTv) { try @@ -489,17 +532,30 @@ namespace Ombi.Schedule.Jobs.Ombi { banner = banner.Replace("http", "https"); // Always use the Https banners } - AddImageInsideTable(sb, banner); + + var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId); + if (tvInfo != null && tvInfo.backdrop_path.HasValue()) + { - sb.Append(""); - sb.Append( - "
"); + AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w500{tvInfo.backdrop_path}"); + } + else + { + AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/"); + } + AddPosterInsideTable(sb, banner); + AddMediaServerUrl(sb, t.Url, banner); + AddInfoTable(sb); - var title = $"{t.Title} ({t.ReleaseYear})"; - - Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); - Header(sb, 3, title); - EndTag(sb, "a"); + var title = ""; + if (!String.IsNullOrEmpty(info.premiered) && info.premiered.Length > 4) + { + title = $"{t.Title} ({info.premiered.Remove(4)})"; + } else + { + title = $"{t.Title}"; + } + AddTitle(sb, $"https://www.imdb.com/title/{info.externals.imdb}/", title); // Group by the season number var results = t.Episodes.GroupBy(p => p.SeasonNumber, @@ -511,32 +567,29 @@ namespace Ombi.Schedule.Jobs.Ombi ); // Group the episodes + var finalsb = new StringBuilder(); 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}"); + var episodeString = BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber)); + finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString}"); + finalsb.Append("
"); } + var summary = info.summary; + if (summary.Length > 280) + { + summary = summary.Remove(280); + summary = summary + "...

"; + } + AddTvParagraph(sb, finalsb.ToString(), summary); + if (info.genres.Any()) { - AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); + AddGenres(sb, $"Genres: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); } + count += 1; - AddParagraph(sb, info.summary); } catch (Exception e) { @@ -546,9 +599,57 @@ namespace Ombi.Schedule.Jobs.Ombi { EndLoopHtml(sb); } - } - sb.Append("


"); + if (count == 2) + { + count = 0; + sb.Append("
"); foreach (var t in orderedTv) { try @@ -581,26 +682,44 @@ namespace Ombi.Schedule.Jobs.Ombi { continue; } + 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( - ""); + sb.Append(""); + } } - sb.Append("
"); + var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId); + if (tvInfo != null && tvInfo.backdrop_path.HasValue()) + { - Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); - Header(sb, 3, t.Title); - EndTag(sb, "a"); + AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w500{tvInfo.backdrop_path}"); + } + else + { + AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/"); + } + AddPosterInsideTable(sb, banner); + AddMediaServerUrl(sb, t.Url, banner); + AddInfoTable(sb); + + var title = ""; + if (!String.IsNullOrEmpty(info.premiered) && info.premiered.Length > 4) + { + title = $"{t.Title} ({info.premiered.Remove(4)})"; + } + else + { + title = $"{t.Title}"; + } + AddTitle(sb, $"https://www.imdb.com/title/{info.externals.imdb}/", title); // Group by the season number var results = t.Episodes?.GroupBy(p => p.SeasonNumber, @@ -612,32 +731,29 @@ namespace Ombi.Schedule.Jobs.Ombi ); // Group the episodes + var finalsb = new StringBuilder(); 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}"); + var episodeString = BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber)); + finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString}"); + finalsb.Append("
"); } + var summary = info.summary; + if (summary.Length > 280) + { + summary = summary.Remove(280); + summary = summary + "...

"; + } + AddTvParagraph(sb, finalsb.ToString(), summary); + if (info.genres.Any()) { - AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); + AddGenres(sb, $"Genres: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); } + count += 1; - AddParagraph(sb, info.summary); } catch (Exception e) { @@ -647,19 +763,28 @@ namespace Ombi.Schedule.Jobs.Ombi { EndLoopHtml(sb); } + + if (count == 2) + { + count = 0; + 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(""); sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); } protected bool ValidateConfiguration(EmailNotificationSettings settings) diff --git a/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexRecentlyAddedSync.cs b/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexRecentlyAddedSync.cs new file mode 100644 index 000000000..34f5562a6 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexRecentlyAddedSync.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Plex +{ + public interface IPlexRecentlyAddedSync : IBaseJob + { + void Start(); + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs index 0292b6b54..334b51e4b 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs @@ -37,6 +37,7 @@ using Ombi.Api.Plex.Models; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; using Ombi.Helpers; +using Ombi.Schedule.Jobs.Ombi; using Ombi.Schedule.Jobs.Plex.Interfaces; using Ombi.Store.Entities; using Ombi.Store.Repository; @@ -46,13 +47,14 @@ namespace Ombi.Schedule.Jobs.Plex public class PlexContentSync : IPlexContentSync { public PlexContentSync(ISettingsService plex, IPlexApi plexApi, ILogger logger, IPlexContentRepository repo, - IPlexEpisodeSync epsiodeSync) + IPlexEpisodeSync epsiodeSync, IRefreshMetadata metadataRefresh) { Plex = plex; PlexApi = plexApi; Logger = logger; Repo = repo; EpisodeSync = epsiodeSync; + Metadata = metadataRefresh; plex.ClearCache(); } @@ -61,6 +63,7 @@ namespace Ombi.Schedule.Jobs.Plex private ILogger Logger { get; } private IPlexContentRepository Repo { get; } private IPlexEpisodeSync EpisodeSync { get; } + private IRefreshMetadata Metadata { get; } public async Task CacheContent(bool recentlyAddedSearch = false) { @@ -87,6 +90,7 @@ namespace Ombi.Schedule.Jobs.Plex Logger.LogInformation("Starting EP Cacher"); BackgroundJob.Enqueue(() => EpisodeSync.Start()); + BackgroundJob.Enqueue(() => Metadata.Start()); } private async Task StartTheCache(PlexSettings plexSettings, bool recentlyAddedSearch) @@ -115,187 +119,30 @@ namespace Ombi.Schedule.Jobs.Plex var contentToAdd = new HashSet(); foreach (var content in allContent) { + if (content.viewGroup.Equals(PlexMediaType.Episode.ToString(), StringComparison.CurrentCultureIgnoreCase)) + { + Logger.LogInformation("Found some episodes, this must be a recently added sync"); + foreach (var epInfo in content.Metadata) + { + var grandParentKey = epInfo.grandparentRatingKey; + // Lookup the rating key + var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri, grandParentKey); + var show = showMetadata.MediaContainer.Metadata.FirstOrDefault(); + if(show == null) + { + continue; + } + + await ProcessTvShow(servers, show, contentToAdd, recentlyAddedSearch); + } + } if (content.viewGroup.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)) { // Process Shows Logger.LogInformation("Processing TV Shows"); foreach (var show in content.Metadata ?? new Metadata[] { }) { - var seasonList = await PlexApi.GetSeasons(servers.PlexAuthToken, servers.FullUri, - show.ratingKey); - var seasonsContent = new List(); - foreach (var season in seasonList.MediaContainer.Metadata) - { - seasonsContent.Add(new PlexSeasonsContent - { - ParentKey = season.parentRatingKey, - SeasonKey = season.ratingKey, - SeasonNumber = season.index, - PlexContentId = show.ratingKey - }); - } - - // Do we already have this item? - // Let's try and match - var existingContent = await Repo.GetFirstContentByCustom(x => x.Title == show.title - && x.ReleaseYear == show.year.ToString() - && x.Type == PlexMediaTypeEntity.Show); - - // Just double check the rating key, since this is our unique constraint - var existingKey = await Repo.GetByKey(show.ratingKey); - - if (existingKey != null) - { - // Damn son. - // Let's check if they match up - var doesMatch = show.title.Equals(existingKey.Title, - StringComparison.CurrentCulture); - if (!doesMatch) - { - // Something fucked up on Plex at somepoint... Damn, rebuild of lib maybe? - // Lets delete the matching key - await Repo.Delete(existingKey); - existingKey = null; - } - } - - if (existingContent != null) - { - // Just check the key - if (existingKey != null) - { - // The rating key is all good! - } - else - { - // This means the rating key has changed somehow. - // Should probably delete this and get the new one - var oldKey = existingContent.Key; - Repo.DeleteWithoutSave(existingContent); - - // Because we have changed the rating key, we need to change all children too - var episodeToChange = Repo.GetAllEpisodes().Where(x => x.GrandparentKey == oldKey); - if (episodeToChange.Any()) - { - foreach (var e in episodeToChange) - { - Repo.DeleteWithoutSave(e); - } - } - await Repo.SaveChangesAsync(); - existingContent = null; - } - } - // The ratingKey keeps changing... - //var existingContent = await Repo.GetByKey(show.ratingKey); - if (existingContent != null) - { - try - { - Logger.LogInformation("We already have show {0} checking for new seasons", - existingContent.Title); - // Ok so we have it, let's check if there are any new seasons - var itemAdded = false; - foreach (var season in seasonsContent) - { - var seasonExists = - existingContent.Seasons.FirstOrDefault(x => x.SeasonKey == season.SeasonKey); - - if (seasonExists != null) - { - // We already have this season - continue; - } - - existingContent.Seasons.Add(season); - itemAdded = true; - } - - if (itemAdded) await Repo.Update(existingContent); - } - catch (Exception e) - { - Logger.LogError(LoggingEvents.PlexContentCacher, e, - "Exception when adding new seasons to title {0}", existingContent.Title); - } - } - else - { - try - { - Logger.LogInformation("New show {0}, so add it", show.title); - - // Get the show metadata... This sucks since the `metadata` var contains all information about the show - // But it does not contain the `guid` property that we need to pull out thetvdb id... - var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri, - show.ratingKey); - var providerIds = - PlexHelper.GetProviderIdFromPlexGuid(showMetadata.MediaContainer.Metadata.FirstOrDefault() - .guid); - - var item = new PlexServerContent - { - AddedAt = DateTime.Now, - Key = show.ratingKey, - ReleaseYear = show.year.ToString(), - Type = PlexMediaTypeEntity.Show, - Title = show.title, - Url = PlexHelper.GetPlexMediaUrl(servers.MachineIdentifier, show.ratingKey), - Seasons = new List() - }; - if (providerIds.Type == ProviderType.ImdbId) - { - item.ImdbId = providerIds.ImdbId; - } - if (providerIds.Type == ProviderType.TheMovieDbId) - { - item.TheMovieDbId = providerIds.TheMovieDb; - } - if (providerIds.Type == ProviderType.TvDbId) - { - item.TvDbId = providerIds.TheTvDb; - } - - // Let's just double check to make sure we do not have it now we have some id's - var existingImdb = false; - var existingMovieDbId = false; - var existingTvDbId = false; - if (item.ImdbId.HasValue()) - { - existingImdb = await Repo.GetAll().AnyAsync(x => - x.ImdbId == item.ImdbId && x.Type == PlexMediaTypeEntity.Show); - } - if (item.TheMovieDbId.HasValue()) - { - existingMovieDbId = await Repo.GetAll().AnyAsync(x => - x.TheMovieDbId == item.TheMovieDbId && x.Type == PlexMediaTypeEntity.Show); - } - if (item.TvDbId.HasValue()) - { - existingTvDbId = await Repo.GetAll().AnyAsync(x => - x.TvDbId == item.TvDbId && x.Type == PlexMediaTypeEntity.Show); - } - if (existingImdb || existingTvDbId || existingMovieDbId) - { - // We already have it! - continue; - } - - item.Seasons.ToList().AddRange(seasonsContent); - - contentToAdd.Add(item); - } - catch (Exception e) - { - Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding tv show {0}", - show.title); - } - } - if (contentToAdd.Count > 500) - { - await Repo.AddRange(contentToAdd); - contentToAdd.Clear(); - } + await ProcessTvShow(servers, show, contentToAdd, recentlyAddedSearch); } } if (content.viewGroup.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase)) @@ -382,6 +229,193 @@ namespace Ombi.Schedule.Jobs.Plex } } + private async Task ProcessTvShow(PlexServers servers, Metadata show, HashSet contentToAdd, bool recentlyAdded) + { + var seasonList = await PlexApi.GetSeasons(servers.PlexAuthToken, servers.FullUri, + show.ratingKey); + var seasonsContent = new List(); + foreach (var season in seasonList.MediaContainer.Metadata) + { + seasonsContent.Add(new PlexSeasonsContent + { + ParentKey = season.parentRatingKey, + SeasonKey = season.ratingKey, + SeasonNumber = season.index, + PlexContentId = show.ratingKey + }); + } + + // Do we already have this item? + // Let's try and match + var existingContent = await Repo.GetFirstContentByCustom(x => x.Title == show.title + && x.ReleaseYear == show.year.ToString() + && x.Type == PlexMediaTypeEntity.Show); + + // Just double check the rating key, since this is our unique constraint + var existingKey = await Repo.GetByKey(show.ratingKey); + + if (existingKey != null) + { + // Damn son. + // Let's check if they match up + var doesMatch = show.title.Equals(existingKey.Title, + StringComparison.CurrentCulture); + if (!doesMatch) + { + // Something fucked up on Plex at somepoint... Damn, rebuild of lib maybe? + // Lets delete the matching key + await Repo.Delete(existingKey); + existingKey = null; + } + } + + if (existingContent != null) + { + // Just check the key + if (existingKey != null) + { + // The rating key is all good! + } + else + { + // This means the rating key has changed somehow. + // Should probably delete this and get the new one + var oldKey = existingContent.Key; + Repo.DeleteWithoutSave(existingContent); + + // Because we have changed the rating key, we need to change all children too + var episodeToChange = Repo.GetAllEpisodes().Where(x => x.GrandparentKey == oldKey); + if (episodeToChange.Any()) + { + foreach (var e in episodeToChange) + { + Repo.DeleteWithoutSave(e); + } + } + + await Repo.SaveChangesAsync(); + existingContent = null; + } + } + + // The ratingKey keeps changing... + //var existingContent = await Repo.GetByKey(show.ratingKey); + if (existingContent != null) + { + try + { + Logger.LogInformation("We already have show {0} checking for new seasons", + existingContent.Title); + // Ok so we have it, let's check if there are any new seasons + var itemAdded = false; + foreach (var season in seasonsContent) + { + var seasonExists = + existingContent.Seasons.FirstOrDefault(x => x.SeasonKey == season.SeasonKey); + + if (seasonExists != null) + { + // We already have this season + continue; + } + + existingContent.Seasons.Add(season); + itemAdded = true; + } + + if (itemAdded) await Repo.Update(existingContent); + } + catch (Exception e) + { + Logger.LogError(LoggingEvents.PlexContentCacher, e, + "Exception when adding new seasons to title {0}", existingContent.Title); + } + } + else + { + try + { + Logger.LogInformation("New show {0}, so add it", show.title); + + // Get the show metadata... This sucks since the `metadata` var contains all information about the show + // But it does not contain the `guid` property that we need to pull out thetvdb id... + var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri, + show.ratingKey); + var providerIds = + PlexHelper.GetProviderIdFromPlexGuid(showMetadata.MediaContainer.Metadata.FirstOrDefault() + .guid); + + var item = new PlexServerContent + { + AddedAt = DateTime.Now, + Key = show.ratingKey, + ReleaseYear = show.year.ToString(), + Type = PlexMediaTypeEntity.Show, + Title = show.title, + Url = PlexHelper.GetPlexMediaUrl(servers.MachineIdentifier, show.ratingKey), + Seasons = new List() + }; + if (providerIds.Type == ProviderType.ImdbId) + { + item.ImdbId = providerIds.ImdbId; + } + + if (providerIds.Type == ProviderType.TheMovieDbId) + { + item.TheMovieDbId = providerIds.TheMovieDb; + } + + if (providerIds.Type == ProviderType.TvDbId) + { + item.TvDbId = providerIds.TheTvDb; + } + + // Let's just double check to make sure we do not have it now we have some id's + var existingImdb = false; + var existingMovieDbId = false; + var existingTvDbId = false; + if (item.ImdbId.HasValue()) + { + existingImdb = await Repo.GetAll().AnyAsync(x => + x.ImdbId == item.ImdbId && x.Type == PlexMediaTypeEntity.Show); + } + + if (item.TheMovieDbId.HasValue()) + { + existingMovieDbId = await Repo.GetAll().AnyAsync(x => + x.TheMovieDbId == item.TheMovieDbId && x.Type == PlexMediaTypeEntity.Show); + } + + if (item.TvDbId.HasValue()) + { + existingTvDbId = await Repo.GetAll().AnyAsync(x => + x.TvDbId == item.TvDbId && x.Type == PlexMediaTypeEntity.Show); + } + + if (existingImdb || existingTvDbId || existingMovieDbId) + { + // We already have it! + return; + } + + item.Seasons.ToList().AddRange(seasonsContent); + + contentToAdd.Add(item); + } + catch (Exception e) + { + Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding tv show {0}", + show.title); + } + } + + if (contentToAdd.Count > 500 || recentlyAdded) + { + await Repo.AddRange(contentToAdd); + contentToAdd.Clear(); + } + } + /// /// Gets all the library sections. /// If the user has specified only certain libraries then we will only look for those diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs index 7d0fde4c7..8e8cce411 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs @@ -50,12 +50,13 @@ namespace Ombi.Schedule.Jobs.Plex await Cache(server); } - BackgroundJob.Enqueue(() => _availabilityChecker.Start()); } catch (Exception e) { _log.LogError(LoggingEvents.Cacher, e, "Caching Episodes Failed"); } + + BackgroundJob.Enqueue(() => _availabilityChecker.Start()); } private async Task Cache(PlexServers settings) diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexMediaType.cs b/src/Ombi.Schedule/Jobs/Plex/PlexMediaType.cs index bf8ac1a75..2e2e9229e 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexMediaType.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexMediaType.cs @@ -31,6 +31,7 @@ namespace Ombi.Schedule.Jobs public enum PlexMediaType { Movie = 0, - Show = 1 + Show = 1, + Episode = 2, } } \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexRecentlyAddedSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexRecentlyAddedSync.cs new file mode 100644 index 000000000..51596f891 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Plex/PlexRecentlyAddedSync.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using Hangfire; + +namespace Ombi.Schedule.Jobs.Plex +{ + public class PlexRecentlyAddedSync : IPlexRecentlyAddedSync + { + public PlexRecentlyAddedSync(IPlexContentSync sync) + { + _sync = sync; + } + + private readonly IPlexContentSync _sync; + + public void Start() + { + BackgroundJob.Enqueue(() => _sync.CacheContent(true)); + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _sync?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs index c4fcb8ceb..2e56f17db 100644 --- a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs +++ b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs @@ -21,7 +21,7 @@ namespace Ombi.Settings.Settings.Models } public static string PlexContent(JobSettings s) { - return Get(s.PlexContentSync, Cron.HourInterval(6)); + return Get(s.PlexContentSync, Cron.Daily(2)); } public static string PlexRecentlyAdded(JobSettings s) { @@ -50,7 +50,7 @@ namespace Ombi.Settings.Settings.Models } public static string RefreshMetadata(JobSettings s) { - return Get(s.RefreshMetadata, Cron.Daily(3)); + return Get(s.RefreshMetadata, Cron.DayInterval(2)); } private static string Get(string settings, string defaultCron) diff --git a/src/Ombi.Store/Entities/EmbyContent.cs b/src/Ombi.Store/Entities/EmbyContent.cs index 4506ef071..348573f28 100644 --- a/src/Ombi.Store/Entities/EmbyContent.cs +++ b/src/Ombi.Store/Entities/EmbyContent.cs @@ -51,6 +51,15 @@ namespace Ombi.Store.Entities public string Url { get; set; } public ICollection Episodes { get; set; } + + [NotMapped] + public bool HasImdb => !string.IsNullOrEmpty(ImdbId); + + [NotMapped] + public bool HasTvDb => !string.IsNullOrEmpty(TvDbId); + + [NotMapped] + public bool HasTheMovieDb => !string.IsNullOrEmpty(TheMovieDbId); } public enum EmbyMediaType diff --git a/src/Ombi.Tests/IdentityControllerTests.cs b/src/Ombi.Tests/IdentityControllerTests.cs index 978ec769c..72e76842d 100644 --- a/src/Ombi.Tests/IdentityControllerTests.cs +++ b/src/Ombi.Tests/IdentityControllerTests.cs @@ -1,132 +1,134 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AutoMapper; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Moq; -using NUnit.Framework; -using Ombi.Api.Emby; -using Ombi.Api.Plex; -using Ombi.Config; -using Ombi.Controllers; -using Ombi.Core.Authentication; -using Ombi.Core.Settings; -using Ombi.Core.Settings.Models.External; -using Ombi.Models; -using Ombi.Notifications; -using Ombi.Schedule.Jobs.Ombi; -using Ombi.Settings.Settings.Models; -using Ombi.Settings.Settings.Models.Notifications; -using Ombi.Store.Context; -using Ombi.Store.Entities; +//using System.Collections.Generic; +//using System.Linq; +//using System.Threading; +//using System.Threading.Tasks; +//using AutoMapper; +//using Microsoft.AspNetCore.Http; +//using Microsoft.AspNetCore.Http.Features.Authentication; +//using Microsoft.AspNetCore.Identity; +//using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +//using Microsoft.EntityFrameworkCore; +//using Microsoft.EntityFrameworkCore.Infrastructure; +//using Microsoft.Extensions.DependencyInjection; +//using Microsoft.Extensions.Options; +//using Moq; +//using NUnit.Framework; +//using Ombi.Api.Emby; +//using Ombi.Api.Plex; +//using Ombi.Config; +//using Ombi.Controllers; +//using Ombi.Core.Authentication; +//using Ombi.Core.Settings; +//using Ombi.Core.Settings.Models.External; +//using Ombi.Models; +//using Ombi.Notifications; +//using Ombi.Schedule.Jobs.Ombi; +//using Ombi.Settings.Settings.Models; +//using Ombi.Settings.Settings.Models.Notifications; +//using Ombi.Store.Context; +//using Ombi.Store.Entities; -namespace Ombi.Tests -{ - [TestFixture] - [Ignore("Need to sort out the DB, looks like it's using the real one...")] - public class IdentityControllerTests - { - [SetUp] - public void Setup() - { - _plexApi = new Mock(); - _embyApi = new Mock(); - _mapper = new Mock(); - _emailProvider = new Mock(); - _emailSettings = new Mock>(); - _customizationSettings = new Mock>(); - _welcomeEmail = new Mock(); - _embySettings = new Mock>(); - _plexSettings = new Mock>(); +//namespace Ombi.Tests +//{ +// [TestFixture] +// [Ignore("Need to sort out the DB, looks like it's using the real one...")] +// public class IdentityControllerTests +// { +// [SetUp] +// public void Setup() +// { +// _plexApi = new Mock(); +// _embyApi = new Mock(); +// _mapper = new Mock(); +// _emailProvider = new Mock(); +// _emailSettings = new Mock>(); +// _customizationSettings = new Mock>(); +// _welcomeEmail = new Mock(); +// _embySettings = new Mock>(); +// _plexSettings = new Mock>(); - var services = new ServiceCollection(); - services.AddEntityFrameworkInMemoryDatabase() - .AddDbContext(); - services.AddIdentity() - .AddEntityFrameworkStores().AddUserManager(); +// var services = new ServiceCollection(); +// services.AddEntityFrameworkInMemoryDatabase() +// .AddDbContext(); +// services.AddIdentity() +// .AddEntityFrameworkStores().AddUserManager(); - services.AddTransient(x => _plexApi.Object); - services.AddTransient(x => _embyApi.Object); - services.AddTransient(x => _customizationSettings.Object); - services.AddTransient(x => _welcomeEmail.Object); - services.AddTransient(x => _emailSettings.Object); - services.AddTransient(x => _emailProvider.Object); - services.AddTransient(x => _mapper.Object); - services.AddTransient(x => _embySettings.Object); - services.AddTransient(x => _plexSettings.Object); +// services.AddTransient(x => _plexApi.Object); +// services.AddTransient(x => _embyApi.Object); +// services.AddTransient(x => _customizationSettings.Object); +// services.AddTransient(x => _welcomeEmail.Object); +// services.AddTransient(x => _emailSettings.Object); +// services.AddTransient(x => _emailProvider.Object); +// services.AddTransient(x => _mapper.Object); +// services.AddTransient(x => _embySettings.Object); +// services.AddTransient(x => _plexSettings.Object); - services.Configure(options => - { - options.Password.RequireDigit = false; - options.Password.RequiredLength = 1; - options.Password.RequireLowercase = false; - options.Password.RequireNonAlphanumeric = false; - options.Password.RequireUppercase = false; - options.User.AllowedUserNameCharacters = string.Empty; - }); +// services.Configure(options => +// { +// options.Password.RequireDigit = false; +// options.Password.RequiredLength = 1; +// options.Password.RequireLowercase = false; +// options.Password.RequireNonAlphanumeric = false; +// options.Password.RequireUppercase = false; +// options.User.AllowedUserNameCharacters = string.Empty; +// }); - // Taken from https://github.com/aspnet/MusicStore/blob/dev/test/MusicStore.Test/ManageControllerTest.cs (and modified) - var context = new DefaultHttpContext(); - context.Features.Set(new HttpAuthenticationFeature()); - services.AddSingleton(h => new HttpContextAccessor { HttpContext = context }); - _serviceProvider = services.BuildServiceProvider(); - _userManager = _serviceProvider.GetRequiredService(); - - Controller = new IdentityController(_userManager, _mapper.Object, _serviceProvider.GetService>(), _emailProvider.Object, - _emailSettings.Object, _customizationSettings.Object,_welcomeEmail.Object, null, null, null, null, null, null, null, null); - } +// // Taken from https://github.com/aspnet/MusicStore/blob/dev/test/MusicStore.Test/ManageControllerTest.cs (and modified) +// var context = new DefaultHttpContext(); +// context.Features.Set(new HttpAuthenticationFeature()); +// services.AddSingleton(h => new HttpContextAccessor { HttpContext = context }); +// _serviceProvider = services.BuildServiceProvider(); +// _userManager = _serviceProvider.GetRequiredService(); - private OmbiUserManager _userManager; - private Mock _emailProvider; - private Mock> _emailSettings; - private Mock> _customizationSettings; - private Mock> _embySettings; - private Mock> _plexSettings; - private Mock _welcomeEmail; - private Mock _mapper; - private Mock _plexApi; - private Mock _embyApi; - private ServiceProvider _serviceProvider; +// Controller = new IdentityController(_userManager, _mapper.Object, +// _serviceProvider.GetService>(), _emailProvider.Object, +// _emailSettings.Object, _customizationSettings.Object, _welcomeEmail.Object, null, null, null, null, +// null, null, null, null, null); +// } - private IdentityController Controller { get; set; } +// private OmbiUserManager _userManager; +// private Mock _emailProvider; +// private Mock> _emailSettings; +// private Mock> _customizationSettings; +// private Mock> _embySettings; +// private Mock> _plexSettings; +// private Mock _welcomeEmail; +// private Mock _mapper; +// private Mock _plexApi; +// private Mock _embyApi; +// private ServiceProvider _serviceProvider; - [Test] - public async Task CreateWizardUser_Should_CreateUser_WhenThereAreNoOtherUsers() - { - var model = new CreateUserWizardModel() - { - Password = "a", - Username = "b" - }; +// private IdentityController Controller { get; set; } - var result = await Controller.CreateWizardUser(model); +// [Test] +// public async Task CreateWizardUser_Should_CreateUser_WhenThereAreNoOtherUsers() +// { +// var model = new CreateUserWizardModel() +// { +// Password = "a", +// Username = "b" +// }; - Assert.That(result, Is.True); - } +// var result = await Controller.CreateWizardUser(model); - [Test] - public async Task CreateWizardUser_ShouldNot_CreateUser_WhenThereAreOtherUsers() - { - var um = _serviceProvider.GetService(); - var r = await um.CreateAsync(new OmbiUser { UserName = "aaaa",UserType = UserType.LocalUser}, "bbb"); - var model = new CreateUserWizardModel - { - Password = "a", - Username = "b" - }; +// Assert.That(result, Is.True); +// } - var result = await Controller.CreateWizardUser(model); +// [Test] +// public async Task CreateWizardUser_ShouldNot_CreateUser_WhenThereAreOtherUsers() +// { +// var um = _serviceProvider.GetService(); +// var r = await um.CreateAsync(new OmbiUser { UserName = "aaaa",UserType = UserType.LocalUser}, "bbb"); +// var model = new CreateUserWizardModel +// { +// Password = "a", +// Username = "b" +// }; - Assert.That(result, Is.False); - } - } -} +// var result = await Controller.CreateWizardUser(model); + +// Assert.That(result, Is.False); +// } +// } +//} diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index 5d0a89992..0b24bd55c 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -18,5 +18,6 @@ namespace Ombi.Api.TheMovieDb Task> SimilarMovies(int movieId); Task Find(string externalId, ExternalSource source); Task GetTvExternals(int theMovieDbId); + Task GetTVInfo(string themoviedbid); } } \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/TvInfo.cs b/src/Ombi.TheMovieDbApi/Models/TvInfo.cs new file mode 100644 index 000000000..5692fcc17 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/TvInfo.cs @@ -0,0 +1,74 @@ +namespace Ombi.Api.TheMovieDb.Models +{ + public class TvInfo + { + public string backdrop_path { get; set; } + public Created_By[] created_by { get; set; } + public int[] episode_run_time { get; set; } + public string first_air_date { get; set; } + public Genre[] genres { get; set; } + public string homepage { get; set; } + public int id { get; set; } + public bool in_production { get; set; } + public string[] languages { get; set; } + public string last_air_date { get; set; } + public string name { get; set; } + public Network[] networks { get; set; } + public int number_of_episodes { get; set; } + public int number_of_seasons { get; set; } + public string[] origin_country { get; set; } + public string original_language { get; set; } + public string original_name { get; set; } + public string overview { get; set; } + public float popularity { get; set; } + public string poster_path { get; set; } + public Production_Companies[] production_companies { get; set; } + public Season[] seasons { get; set; } + public string status { get; set; } + public string type { get; set; } + public float vote_average { get; set; } + public int vote_count { get; set; } + } + + public class Created_By + { + public int id { get; set; } + public string name { get; set; } + public int gender { get; set; } + public string profile_path { get; set; } + } + + public class Genre + { + public int id { get; set; } + public string name { get; set; } + } + + public class Network + { + public string name { get; set; } + public int id { get; set; } + public string logo_path { get; set; } + public string origin_country { get; set; } + } + + public class Production_Companies + { + public int id { get; set; } + public string logo_path { get; set; } + public string name { get; set; } + public string origin_country { get; set; } + } + + public class Season + { + public string air_date { get; set; } + public int episode_count { get; set; } + public int id { get; set; } + public string name { get; set; } + public string overview { get; set; } + public string poster_path { get; set; } + public int season_number { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index ccd0e52e6..af9423f5d 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -129,6 +129,15 @@ namespace Ombi.Api.TheMovieDb var result = await Api.Request>(request); return Mapper.Map>(result.results); } + + public async Task GetTVInfo(string themoviedbid) + { + var request = new Request($"/tv/{themoviedbid}", BaseUri, HttpMethod.Get); + request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + AddRetry(request); + + return await Api.Request(request); + } private static void AddRetry(Request request) { request.Retry = true; diff --git a/src/Ombi/ClientApp/app/app.component.html b/src/Ombi/ClientApp/app/app.component.html index 0db13fa9c..2f2c5a5a1 100644 --- a/src/Ombi/ClientApp/app/app.component.html +++ b/src/Ombi/ClientApp/app/app.component.html @@ -1,4 +1,4 @@ -
+