From 46c7de379c872f757847a311b21714e905466360 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 6 Jul 2024 15:57:14 -0700 Subject: [PATCH] New: Group updates for the same series for Kodi and Emby / Jellyfin --- .../Xbmc/OnDownloadFixture.cs | 4 + .../MediaBrowser/MediaBrowser.cs | 71 +++++++----- .../Notifications/MediaServerUpdateQueue.cs | 105 ++++++++++++++++++ .../Notifications/Plex/Server/PlexServer.cs | 77 ++----------- src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs | 52 ++++++--- 5 files changed, 199 insertions(+), 110 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs index 09063bc4d..729c2e326 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc Subject.Definition = new NotificationDefinition(); Subject.Definition.Settings = new XbmcSettings { + Host = "localhost", UpdateLibrary = true }; } @@ -49,6 +50,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc Subject.Definition.Settings = new XbmcSettings { + Host = "localhost", UpdateLibrary = true, CleanLibrary = true }; @@ -58,6 +60,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc public void should_not_clean_if_no_episode_was_replaced() { Subject.OnDownload(_downloadMessage); + Subject.ProcessQueue(); Mocker.GetMock().Verify(v => v.Clean(It.IsAny()), Times.Never()); } @@ -67,6 +70,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc { GivenOldFiles(); Subject.OnDownload(_downloadMessage); + Subject.ProcessQueue(); Mocker.GetMock().Verify(v => v.Clean(It.IsAny()), Times.Once()); } diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs index 5ab3b0764..bcdbe237c 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; +using System.Linq; using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tv; @@ -9,10 +12,18 @@ namespace NzbDrone.Core.Notifications.Emby public class MediaBrowser : NotificationBase { private readonly IMediaBrowserService _mediaBrowserService; + private readonly MediaServerUpdateQueue _updateQueue; + private readonly Logger _logger; - public MediaBrowser(IMediaBrowserService mediaBrowserService) + private static string Created = "Created"; + private static string Deleted = "Deleted"; + private static string Modified = "Modified"; + + public MediaBrowser(IMediaBrowserService mediaBrowserService, ICacheManager cacheManager, Logger logger) { _mediaBrowserService = mediaBrowserService; + _updateQueue = new MediaServerUpdateQueue(cacheManager); + _logger = logger; } public override string Link => "https://emby.media/"; @@ -33,10 +44,7 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, EPISODE_DOWNLOADED_TITLE_BRANDED, message.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, message.Series, "Created"); - } + UpdateIfEnabled(message.Series, Created); } public override void OnImportComplete(ImportCompleteMessage message) @@ -46,18 +54,12 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, IMPORT_COMPLETE_TITLE_BRANDED, message.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, message.Series, "Created"); - } + UpdateIfEnabled(message.Series, Created); } public override void OnRename(Series series, List renamedFiles) { - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, series, "Modified"); - } + UpdateIfEnabled(series, Modified); } public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) @@ -67,10 +69,7 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, EPISODE_DELETED_TITLE_BRANDED, deleteMessage.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, deleteMessage.Series, "Deleted"); - } + UpdateIfEnabled(deleteMessage.Series, Deleted); } public override void OnSeriesAdd(SeriesAddMessage message) @@ -80,10 +79,7 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, SERIES_ADDED_TITLE_BRANDED, message.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, message.Series, "Created"); - } + UpdateIfEnabled(message.Series, Created); } public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) @@ -93,10 +89,7 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, SERIES_DELETED_TITLE_BRANDED, deleteMessage.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, deleteMessage.Series, "Deleted"); - } + UpdateIfEnabled(deleteMessage.Series, Deleted); } public override void OnHealthIssue(HealthCheck.HealthCheck message) @@ -123,6 +116,34 @@ namespace NzbDrone.Core.Notifications.Emby } } + public override void ProcessQueue() + { + _updateQueue.ProcessQueue(Settings.Host, (items) => + { + if (Settings.UpdateLibrary) + { + _logger.Debug("Performing library update for {0} series", items.Count); + + items.ForEach(item => + { + // If there is only one update type for the series use that, otherwise send null and let Emby decide + var updateType = item.Info.Count == 1 ? item.Info.First() : null; + + _mediaBrowserService.Update(Settings, item.Series, updateType); + }); + } + }); + } + + private void UpdateIfEnabled(Series series, string updateType) + { + if (Settings.UpdateLibrary) + { + _logger.Debug("Scheduling library update for series {0} {1}", series.Id, series.Title); + _updateQueue.Add(Settings.Host, series, updateType); + } + } + public override ValidationResult Test() { var failures = new List(); diff --git a/src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs b/src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs new file mode 100644 index 000000000..101adf95e --- /dev/null +++ b/src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Notifications +{ + public class MediaServerUpdateQueue + where TQueueHost : class + { + private class UpdateQueue + { + public Dictionary> Pending { get; } = new Dictionary>(); + public bool Refreshing { get; set; } + } + + private readonly ICached _pendingSeriesCache; + + public MediaServerUpdateQueue(ICacheManager cacheManager) + { + _pendingSeriesCache = cacheManager.GetRollingCache(typeof(TQueueHost), "pendingSeries", TimeSpan.FromDays(1)); + } + + public void Add(string identifier, Series series, TItemInfo info) + { + var queue = _pendingSeriesCache.Get(identifier, () => new UpdateQueue()); + + lock (queue) + { + var item = queue.Pending.TryGetValue(series.Id, out var value) + ? value + : new UpdateQueueItem(series); + + item.Info.Add(info); + + queue.Pending[series.Id] = item; + } + } + + public void ProcessQueue(string identifier, Action>> update) + { + var queue = _pendingSeriesCache.Find(identifier); + + if (queue == null) + { + return; + } + + lock (queue) + { + if (queue.Refreshing) + { + return; + } + + queue.Refreshing = true; + } + + try + { + while (true) + { + List> items; + + lock (queue) + { + if (queue.Pending.Empty()) + { + queue.Refreshing = false; + return; + } + + items = queue.Pending.Values.ToList(); + queue.Pending.Clear(); + } + + update(items); + } + } + catch + { + lock (queue) + { + queue.Refreshing = false; + } + + throw; + } + } + } + + public class UpdateQueueItem + { + public Series Series { get; set; } + public HashSet Info { get; set; } + + public UpdateQueueItem(Series series) + { + Series = series; + Info = new HashSet(); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs index ab8079f7c..ca7cc06b6 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs @@ -18,23 +18,15 @@ namespace NzbDrone.Core.Notifications.Plex.Server { private readonly IPlexServerService _plexServerService; private readonly IPlexTvService _plexTvService; + private readonly MediaServerUpdateQueue _updateQueue; private readonly Logger _logger; - private class PlexUpdateQueue - { - public Dictionary Pending { get; } = new Dictionary(); - public bool Refreshing { get; set; } - } - - private readonly ICached _pendingSeriesCache; - public PlexServer(IPlexServerService plexServerService, IPlexTvService plexTvService, ICacheManager cacheManager, Logger logger) { _plexServerService = plexServerService; _plexTvService = plexTvService; + _updateQueue = new MediaServerUpdateQueue(cacheManager); _logger = logger; - - _pendingSeriesCache = cacheManager.GetRollingCache(GetType(), "pendingSeries", TimeSpan.FromDays(1)); } public override string Link => "https://www.plex.tv/"; @@ -80,66 +72,20 @@ namespace NzbDrone.Core.Notifications.Plex.Server if (Settings.UpdateLibrary) { _logger.Debug("Scheduling library update for series {0} {1}", series.Id, series.Title); - var queue = _pendingSeriesCache.Get(Settings.Host, () => new PlexUpdateQueue()); - lock (queue) - { - queue.Pending[series.Id] = series; - } + _updateQueue.Add(Settings.Host, series, false); } } public override void ProcessQueue() { - var queue = _pendingSeriesCache.Find(Settings.Host); - - if (queue == null) + _updateQueue.ProcessQueue(Settings.Host, (items) => { - return; - } - - lock (queue) - { - if (queue.Refreshing) + if (Settings.UpdateLibrary) { - return; + _logger.Debug("Performing library update for {0} series", items.Count); + _plexServerService.UpdateLibrary(items.Select(i => i.Series), Settings); } - - queue.Refreshing = true; - } - - try - { - while (true) - { - List refreshingSeries; - lock (queue) - { - if (queue.Pending.Empty()) - { - queue.Refreshing = false; - return; - } - - refreshingSeries = queue.Pending.Values.ToList(); - queue.Pending.Clear(); - } - - if (Settings.UpdateLibrary) - { - _logger.Debug("Performing library update for {0} series", refreshingSeries.Count); - _plexServerService.UpdateLibrary(refreshingSeries, Settings); - } - } - } - catch - { - lock (queue) - { - queue.Refreshing = false; - } - - throw; - } + }); } public override ValidationResult Test() @@ -213,13 +159,6 @@ namespace NzbDrone.Core.Notifications.Plex.Server { var result = new List(); - // result.Add(new FieldSelectStringOption - // { - // Value = s.Name, - // Name = s.Name, - // IsDisabled = true - // }); - s.Connections.ForEach(c => { var isSecure = c.Protocol == "https"; diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs index 1bff9d178..51a656545 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Sockets; using FluentValidation.Results; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tv; @@ -12,11 +13,13 @@ namespace NzbDrone.Core.Notifications.Xbmc public class Xbmc : NotificationBase { private readonly IXbmcService _xbmcService; + private readonly MediaServerUpdateQueue _updateQueue; private readonly Logger _logger; - public Xbmc(IXbmcService xbmcService, Logger logger) + public Xbmc(IXbmcService xbmcService, ICacheManager cacheManager, Logger logger) { _xbmcService = xbmcService; + _updateQueue = new MediaServerUpdateQueue(cacheManager); _logger = logger; } @@ -99,6 +102,35 @@ namespace NzbDrone.Core.Notifications.Xbmc public override string Name => "Kodi"; + public override void ProcessQueue() + { + _updateQueue.ProcessQueue(Settings.Host, (items) => + { + _logger.Debug("Performing library update for {0} series", items.Count); + + items.ForEach(item => + { + try + { + if (Settings.UpdateLibrary) + { + _xbmcService.Update(Settings, item.Series); + } + + if (item.Info.Contains(true) && Settings.CleanLibrary) + { + _xbmcService.Clean(Settings); + } + } + catch (SocketException ex) + { + var logMessage = string.Format("Unable to connect to Kodi Host: {0}:{1}", Settings.Host, Settings.Port); + _logger.Debug(ex, logMessage); + } + }); + }); + } + public override ValidationResult Test() { var failures = new List(); @@ -126,22 +158,10 @@ namespace NzbDrone.Core.Notifications.Xbmc private void UpdateAndClean(Series series, bool clean = true) { - try + if (Settings.UpdateLibrary || Settings.CleanLibrary) { - if (Settings.UpdateLibrary) - { - _xbmcService.Update(Settings, series); - } - - if (clean && Settings.CleanLibrary) - { - _xbmcService.Clean(Settings); - } - } - catch (SocketException ex) - { - var logMessage = string.Format("Unable to connect to Kodi Host: {0}:{1}", Settings.Host, Settings.Port); - _logger.Debug(ex, logMessage); + _logger.Debug("Scheduling library update for series {0} {1}", series.Id, series.Title); + _updateQueue.Add(Settings.Host, series, clean); } } }