From 017fa5ad80af2d997e103b5b29cfb3f7d867daae 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 movie for Kodi and Emby / Jellyfin (cherry picked from commit 46c7de379c872f757847a311b21714e905466360) Closes #10150 --- .../Xbmc/OnDownloadFixture.cs | 18 +-- .../Xbmc/UpdateMovieFixture.cs | 4 +- .../MediaBrowser/MediaBrowser.cs | 55 ++++++--- .../Notifications/MediaServerUpdateQueue.cs | 105 ++++++++++++++++++ .../Notifications/Plex/Server/PlexServer.cs | 77 ++----------- src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs | 62 +++++++---- .../Notifications/Xbmc/XbmcService.cs | 4 +- 7 files changed, 210 insertions(+), 115 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 fd11fe3fa..8d525bfef 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnDownloadFixture.cs @@ -33,9 +33,10 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc Subject.Definition = new NotificationDefinition(); Subject.Definition.Settings = new XbmcSettings - { - UpdateLibrary = true - }; + { + Host = "localhost", + UpdateLibrary = true + }; } private void GivenOldFiles() @@ -48,16 +49,18 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc .ToList(); Subject.Definition.Settings = new XbmcSettings - { - UpdateLibrary = true, - CleanLibrary = true - }; + { + Host = "localhost", + UpdateLibrary = true, + CleanLibrary = true + }; } [Test] public void should_not_clean_if_no_movie_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.Test/NotificationTests/Xbmc/UpdateMovieFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/UpdateMovieFixture.cs index 0fcf16e14..d85695046 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/UpdateMovieFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/UpdateMovieFixture.cs @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc .With(s => s.ImdbId = IMDB_ID) .Build(); - Subject.UpdateMovie(_settings, movie); + Subject.Update(_settings, movie); Mocker.GetMock() .Verify(v => v.UpdateLibrary(_settings, It.IsAny()), Times.Once()); @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc .With(s => s.Title = "Not A Real Movie") .Build(); - Subject.UpdateMovie(_settings, fakeMovie); + Subject.Update(_settings, fakeMovie); Mocker.GetMock() .Verify(v => v.UpdateLibrary(_settings, null), Times.Once()); diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs index d9adb47bd..0b866d6f7 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.Movies; @@ -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,18 +44,12 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, MOVIE_DOWNLOADED_TITLE_BRANDED, message.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, message.Movie, "Created"); - } + UpdateIfEnabled(message.Movie, Created); } public override void OnMovieRename(Movie movie, List renamedFiles) { - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, movie, "Modified"); - } + UpdateIfEnabled(movie, Modified); } public override void OnHealthIssue(HealthCheck.HealthCheck message) @@ -80,10 +85,7 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, MOVIE_DELETED_TITLE_BRANDED, deleteMessage.Message); } - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, deleteMessage.Movie, "Deleted"); - } + UpdateIfEnabled(deleteMessage.Movie, Deleted); } } @@ -94,9 +96,34 @@ namespace NzbDrone.Core.Notifications.Emby _mediaBrowserService.Notify(Settings, MOVIE_FILE_DELETED_TITLE_BRANDED, deleteMessage.Message); } + UpdateIfEnabled(deleteMessage.Movie, Deleted); + } + + public override void ProcessQueue() + { + _updateQueue.ProcessQueue(Settings.Host, (items) => + { + if (Settings.UpdateLibrary) + { + _logger.Debug("Performing library update for {0} movies", items.Count); + + items.ForEach(item => + { + // If there is only one update type for the movie use that, otherwise send null and let Emby decide + var updateType = item.Info.Count == 1 ? item.Info.First() : null; + + _mediaBrowserService.Update(Settings, item.Movie, updateType); + }); + } + }); + } + + private void UpdateIfEnabled(Movie movie, string updateType) + { if (Settings.UpdateLibrary) { - _mediaBrowserService.Update(Settings, deleteMessage.Movie, "Deleted"); + _logger.Debug("Scheduling library update for movie {0} {1}", movie.Id, movie.Title); + _updateQueue.Add(Settings.Host, movie, updateType); } } diff --git a/src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs b/src/NzbDrone.Core/Notifications/MediaServerUpdateQueue.cs new file mode 100644 index 000000000..80cf96b3e --- /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.Movies; + +namespace NzbDrone.Core.Notifications +{ + public class MediaServerUpdateQueue + where TQueueHost : class + { + private class UpdateQueue + { + public Dictionary> Pending { get; } = new (); + public bool Refreshing { get; set; } + } + + private readonly ICached _pendingMoviesCache; + + public MediaServerUpdateQueue(ICacheManager cacheManager) + { + _pendingMoviesCache = cacheManager.GetRollingCache(typeof(TQueueHost), "pendingMovies", TimeSpan.FromDays(1)); + } + + public void Add(string identifier, Movie movie, TItemInfo info) + { + var queue = _pendingMoviesCache.Get(identifier, () => new UpdateQueue()); + + lock (queue) + { + var item = queue.Pending.TryGetValue(movie.Id, out var value) + ? value + : new UpdateQueueItem(movie); + + item.Info.Add(info); + + queue.Pending[movie.Id] = item; + } + } + + public void ProcessQueue(string identifier, Action>> update) + { + var queue = _pendingMoviesCache.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 Movie Movie { get; set; } + public HashSet Info { get; set; } + + public UpdateQueueItem(Movie movie) + { + Movie = movie; + Info = new HashSet(); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs index 7cf247e55..4efc5a90b 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 _pendingMoviesCache; - public PlexServer(IPlexServerService plexServerService, IPlexTvService plexTvService, ICacheManager cacheManager, Logger logger) { _plexServerService = plexServerService; _plexTvService = plexTvService; + _updateQueue = new MediaServerUpdateQueue(cacheManager); _logger = logger; - - _pendingMoviesCache = cacheManager.GetRollingCache(GetType(), "pendingSeries", TimeSpan.FromDays(1)); } public override string Link => "https://www.plex.tv/"; @@ -70,66 +62,20 @@ namespace NzbDrone.Core.Notifications.Plex.Server if (Settings.UpdateLibrary) { _logger.Debug("Scheduling library update for movie {0} {1}", movie.Id, movie.Title); - var queue = _pendingMoviesCache.Get(Settings.Host, () => new PlexUpdateQueue()); - lock (queue) - { - queue.Pending[movie.Id] = movie; - } + _updateQueue.Add(Settings.Host, movie, false); } } public override void ProcessQueue() { - var queue = _pendingMoviesCache.Find(Settings.Host); - - if (queue == null) - { - return; - } - - lock (queue) - { - if (queue.Refreshing) - { - return; - } - - queue.Refreshing = true; - } - - try + _updateQueue.ProcessQueue(Settings.Host, (items) => { - while (true) + if (Settings.UpdateLibrary) { - List refreshingMovies; - lock (queue) - { - if (queue.Pending.Empty()) - { - queue.Refreshing = false; - return; - } - - refreshingMovies = queue.Pending.Values.ToList(); - queue.Pending.Clear(); - } - - if (Settings.UpdateLibrary) - { - _logger.Debug("Performing library update for {0} movies", refreshingMovies.Count); - _plexServerService.UpdateLibrary(refreshingMovies, Settings); - } + _logger.Debug("Performing library update for {0} movies", items.Count); + _plexServerService.UpdateLibrary(items.Select(i => i.Movie), Settings); } - } - catch - { - lock (queue) - { - queue.Refreshing = false; - } - - throw; - } + }); } public override ValidationResult Test() @@ -203,13 +149,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 f87ad1f3f..b1d802490 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.Movies; @@ -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; } @@ -34,12 +37,12 @@ namespace NzbDrone.Core.Notifications.Xbmc const string header = "Radarr - Downloaded"; Notify(Settings, header, message.Message); - UpdateAndCleanMovie(message.Movie, message.OldMovieFiles.Any()); + UpdateAndClean(message.Movie, message.OldMovieFiles.Any()); } public override void OnMovieRename(Movie movie, List renamedFiles) { - UpdateAndCleanMovie(movie); + UpdateAndClean(movie); } public override void OnMovieFileDelete(MovieFileDeleteMessage deleteMessage) @@ -47,7 +50,7 @@ namespace NzbDrone.Core.Notifications.Xbmc const string header = "Radarr - Deleted"; Notify(Settings, header, deleteMessage.Message); - UpdateAndCleanMovie(deleteMessage.Movie, true); + UpdateAndClean(deleteMessage.Movie, true); } public override void OnMovieDelete(MovieDeleteMessage deleteMessage) @@ -57,7 +60,7 @@ namespace NzbDrone.Core.Notifications.Xbmc const string header = "Radarr - Deleted"; Notify(Settings, header, deleteMessage.Message); - UpdateAndCleanMovie(deleteMessage.Movie, true); + UpdateAndClean(deleteMessage.Movie, true); } } @@ -83,6 +86,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} movies", items.Count); + + items.ForEach(item => + { + try + { + if (Settings.UpdateLibrary) + { + _xbmcService.Update(Settings, item.Movie); + } + + 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(); @@ -108,24 +140,12 @@ namespace NzbDrone.Core.Notifications.Xbmc } } - private void UpdateAndCleanMovie(Movie movie, bool clean = true) + private void UpdateAndClean(Movie movie, bool clean = true) { - try + if (Settings.UpdateLibrary || Settings.CleanLibrary) { - if (Settings.UpdateLibrary) - { - _xbmcService.UpdateMovie(Settings, movie); - } - - 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 movie {0} {1}", movie.Id, movie.Title); + _updateQueue.Add(Settings.Host, movie, clean); } } } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs index 56092c92e..bf6c6e9c3 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Notifications.Xbmc public interface IXbmcService { void Notify(XbmcSettings settings, string title, string message); - void UpdateMovie(XbmcSettings settings, Movie movie); + void Update(XbmcSettings settings, Movie movie); void Clean(XbmcSettings settings); ValidationFailure Test(XbmcSettings settings, string message); } @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Xbmc _proxy.Notify(settings, title, message); } - public void UpdateMovie(XbmcSettings settings, Movie movie) + public void Update(XbmcSettings settings, Movie movie) { if (CheckIfVideoPlayerOpen(settings)) {