From a1104b826399b17a0c4bf48a409fbef62381ae65 Mon Sep 17 00:00:00 2001 From: Jendrik Weise Date: Sat, 29 Jul 2023 03:40:30 +0200 Subject: [PATCH] New: Update matching movie path in Jellyfin/Emby library (cherry picked from commit ad0dc01cf7ed16ccfa8260717111ad8a44675221) Closes #8898 --- .../MediaBrowser/MediaBrowser.cs | 8 +- .../MediaBrowser/MediaBrowserItems.cs | 30 +++++++ .../MediaBrowser/MediaBrowserProxy.cs | 83 ++++++++++++++++++- .../MediaBrowser/MediaBrowserService.cs | 22 ++++- .../MediaBrowser/MediaBrowserSettings.cs | 10 +++ 5 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserItems.cs diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs index 9beb81e3f..d9adb47bd 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Notifications.Emby if (Settings.UpdateLibrary) { - _mediaBrowserService.UpdateMovies(Settings, message.Movie, "Created"); + _mediaBrowserService.Update(Settings, message.Movie, "Created"); } } @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Notifications.Emby { if (Settings.UpdateLibrary) { - _mediaBrowserService.UpdateMovies(Settings, movie, "Modified"); + _mediaBrowserService.Update(Settings, movie, "Modified"); } } @@ -82,7 +82,7 @@ namespace NzbDrone.Core.Notifications.Emby if (Settings.UpdateLibrary) { - _mediaBrowserService.UpdateMovies(Settings, deleteMessage.Movie, "Deleted"); + _mediaBrowserService.Update(Settings, deleteMessage.Movie, "Deleted"); } } } @@ -96,7 +96,7 @@ namespace NzbDrone.Core.Notifications.Emby if (Settings.UpdateLibrary) { - _mediaBrowserService.UpdateMovies(Settings, deleteMessage.Movie, "Deleted"); + _mediaBrowserService.Update(Settings, deleteMessage.Movie, "Deleted"); } } diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserItems.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserItems.cs new file mode 100644 index 000000000..0e17c30f2 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserItems.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Emby +{ + public class MediaBrowserItems + { + public List Items { get; set; } + } + + public class MediaBrowserItem + { + public string Name { get; set; } + public string Path { get; set; } + + public MediaBrowserProviderIds ProviderIds { get; set; } + } + + public class MediaBrowserProviderIds + { + public string Imdb { get; set; } + public int Tmdb { get; set; } + } + + public enum MediaBrowserMatchQuality + { + Id = 0, + Name = 1, + None = 2 + } +} diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs index ac2ceb586..2991dbf8f 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs @@ -1,6 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; using NLog; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; +using NzbDrone.Core.Movies; namespace NzbDrone.Core.Notifications.Emby { @@ -31,7 +35,62 @@ namespace NzbDrone.Core.Notifications.Emby ProcessRequest(request, settings); } - public void UpdateMovies(MediaBrowserSettings settings, string moviePath, string updateType) + public HashSet GetPaths(MediaBrowserSettings settings, Movie movie) + { + var path = "/Items"; + var url = GetUrl(settings); + + // NameStartsWith uses the sort title, which is not the movie title + var request = new HttpRequestBuilder(url) + .Resource(path) + .AddQueryParam("recursive", "true") + .AddQueryParam("includeItemTypes", "Movie") + .AddQueryParam("fields", "Path,ProviderIds") + .AddQueryParam("years", movie.Year) + .Build(); + + try + { + var paths = ProcessGetRequest(request, settings).Items.GroupBy(item => + { + if (item is { ProviderIds.Tmdb: int tmdbid } && tmdbid != 0 && tmdbid == movie.TmdbId) + { + return MediaBrowserMatchQuality.Id; + } + + if (item is { ProviderIds.Imdb: string imdbid } && imdbid == movie.ImdbId) + { + return MediaBrowserMatchQuality.Id; + } + + if (item is { Name: var name } && name == movie.Title) + { + return MediaBrowserMatchQuality.Name; + } + + return MediaBrowserMatchQuality.None; + }, item => item.Path).OrderBy(group => (int)group.Key).First(); + + if (paths.Key == MediaBrowserMatchQuality.None) + { + _logger.Trace("Could not find movie by name"); + + return new HashSet(); + } + + _logger.Trace("Found movie by name/id: {0}", string.Join(" ", paths)); + + return paths.ToHashSet(); + } + catch (InvalidOperationException) + { + _logger.Trace("Could not find movie by name."); + + return new HashSet(); + } + } + + public void Update(MediaBrowserSettings settings, string moviePath, string updateType) { var path = "/Library/Media/Updated"; var request = BuildRequest(path, settings); @@ -52,6 +111,19 @@ namespace NzbDrone.Core.Notifications.Emby ProcessRequest(request, settings); } + private T ProcessGetRequest(HttpRequest request, MediaBrowserSettings settings) + where T : new() + { + request.Headers.Add("X-MediaBrowser-Token", settings.ApiKey); + + var response = _httpClient.Get(request); + _logger.Trace("Response: {0}", response.Content); + + CheckForError(response); + + return response.Resource; + } + private string ProcessRequest(HttpRequest request, MediaBrowserSettings settings) { request.Headers.Add("X-MediaBrowser-Token", settings.ApiKey); @@ -64,10 +136,15 @@ namespace NzbDrone.Core.Notifications.Emby return response.Content; } - private HttpRequest BuildRequest(string path, MediaBrowserSettings settings) + private string GetUrl(MediaBrowserSettings settings) { var scheme = settings.UseSsl ? "https" : "http"; - var url = $@"{scheme}://{settings.Address}/mediabrowser"; + return $@"{scheme}://{settings.Address}"; + } + + private HttpRequest BuildRequest(string path, MediaBrowserSettings settings) + { + var url = GetUrl(settings); return new HttpRequestBuilder(url).Resource(path).Build(); } diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs index e2e3fc093..b09997207 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Net; using FluentValidation.Results; using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Localization; using NzbDrone.Core.Movies; @@ -12,7 +14,7 @@ namespace NzbDrone.Core.Notifications.Emby public interface IMediaBrowserService { void Notify(MediaBrowserSettings settings, string title, string message); - void UpdateMovies(MediaBrowserSettings settings, Movie movie, string updateType); + void Update(MediaBrowserSettings settings, Movie movie, string updateType); ValidationFailure Test(MediaBrowserSettings settings); } @@ -34,9 +36,23 @@ namespace NzbDrone.Core.Notifications.Emby _proxy.Notify(settings, title, message); } - public void UpdateMovies(MediaBrowserSettings settings, Movie movie, string updateType) + public void Update(MediaBrowserSettings settings, Movie movie, string updateType) { - _proxy.UpdateMovies(settings, movie.Path, updateType); + var paths = _proxy.GetPaths(settings, movie); + + var mappedPath = new OsPath(movie.Path); + + if (settings.MapTo.IsNotNullOrWhiteSpace()) + { + mappedPath = new OsPath(settings.MapTo) + (mappedPath - new OsPath(settings.MapFrom)); + } + + paths.Add(mappedPath.ToString()); + + foreach (var path in paths) + { + _proxy.Update(settings, path, updateType); + } } public ValidationFailure Test(MediaBrowserSettings settings) diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs index 35382cff4..88a587468 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs @@ -13,6 +13,8 @@ namespace NzbDrone.Core.Notifications.Emby RuleFor(c => c.Host).ValidHost(); RuleFor(c => c.ApiKey).NotEmpty(); RuleFor(c => c.UrlBase).ValidUrlBase(); + RuleFor(c => c.MapFrom).NotEmpty().Unless(c => c.MapTo.IsNullOrWhiteSpace()); + RuleFor(c => c.MapTo).NotEmpty().Unless(c => c.MapFrom.IsNullOrWhiteSpace()); } } @@ -49,6 +51,14 @@ namespace NzbDrone.Core.Notifications.Emby [FieldDefinition(6, Label = "NotificationsSettingsUpdateLibrary", HelpText = "NotificationsEmbySettingsUpdateLibraryHelpText", Type = FieldType.Checkbox)] public bool UpdateLibrary { get; set; } + [FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsFrom", HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText", Type = FieldType.Textbox, Advanced = true)] + [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsFrom", "serviceName", "Emby/Jellyfin")] + public string MapFrom { get; set; } + + [FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsTo", HelpText = "NotificationsSettingsUpdateMapPathsToHelpText", Type = FieldType.Textbox, Advanced = true)] + [FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsTo", "serviceName", "Emby/Jellyfin")] + public string MapTo { get; set; } + [JsonIgnore] public string Address => $"{Host.ToUrlHost()}:{Port}{UrlBase}";