From 24206ad0a3095c4bf5c860af516d8543bd6525d6 Mon Sep 17 00:00:00 2001 From: Qstick Date: Thu, 21 Apr 2022 22:49:09 -0500 Subject: [PATCH] New: Support Plex API Path Scan (Similar to autoscan) Closes #4640 Closes #5527 --- .../Plex/Server/PlexServerProxy.cs | 11 ++ .../Plex/Server/PlexServerService.cs | 101 +++++++++++++++--- .../Plex/Server/PlexServerSettings.cs | 9 ++ 3 files changed, 109 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs index e4fab75cc..ca7af6a18 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server List GetMovieSections(PlexServerSettings settings); void Update(int sectionId, PlexServerSettings settings); void UpdateMovie(string metadataId, PlexServerSettings settings); + void UpdatePath(string path, int sectionId, PlexServerSettings settings); string Version(PlexServerSettings settings); List Preferences(PlexServerSettings settings); string GetMetadataId(int sectionId, string imdbId, string language, PlexServerSettings settings); @@ -81,6 +82,16 @@ namespace NzbDrone.Core.Notifications.Plex.Server CheckForError(response); } + public void UpdatePath(string path, int sectionId, PlexServerSettings settings) + { + var resource = $"library/sections/{sectionId}/refresh"; + var request = BuildRequest(resource, HttpMethod.Get, settings); + request.AddQueryParam("path", path); + var response = ProcessRequest(request); + + CheckForError(response); + } + public string Version(PlexServerSettings settings) { var request = BuildRequest("identity", HttpMethod.Get, settings); diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs index eb14090b6..581ac082f 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Text.RegularExpressions; using FluentValidation.Results; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Movies; using NzbDrone.Core.Validation; @@ -23,6 +25,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server { private readonly ICached _versionCache; private readonly ICached _partialUpdateCache; + private readonly ICached _pathScanCache; private readonly IPlexServerProxy _plexServerProxy; private readonly Logger _logger; @@ -30,6 +33,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server { _versionCache = cacheManager.GetCache(GetType(), "versionCache"); _partialUpdateCache = cacheManager.GetCache(GetType(), "partialUpdateCache"); + _pathScanCache = cacheManager.GetCache(GetType(), "pathScanCache"); _plexServerProxy = plexServerProxy; _logger = logger; } @@ -51,32 +55,52 @@ namespace NzbDrone.Core.Notifications.Plex.Server var sections = GetSections(settings); var partialUpdates = _partialUpdateCache.Get(settings.Host, () => PartialUpdatesAllowed(settings, version), TimeSpan.FromHours(2)); + var pathScanCache = _pathScanCache.Get(settings.Host, () => PathUpdatesAllowed(settings, version), TimeSpan.FromHours(2)); - if (partialUpdates) - { - var partiallyUpdated = true; + var pathUpdated = true; + if (pathScanCache) + { foreach (var movie in multipleMovies) { - partiallyUpdated &= UpdatePartialSection(movie, sections, settings); + pathUpdated &= UpdatePath(movie, sections, settings); - if (!partiallyUpdated) + if (!pathUpdated) { break; } } + } - // Only update complete sections if all partial updates failed - if (!partiallyUpdated) + // If we couldn't path update then try partial and full update + if (!pathUpdated) + { + if (partialUpdates) + { + var partiallyUpdated = true; + + foreach (var movie in multipleMovies) + { + partiallyUpdated &= UpdatePartialSection(movie, sections, settings); + + if (!partiallyUpdated) + { + break; + } + } + + // Only update complete sections if all partial updates failed + if (!partiallyUpdated) + { + _logger.Debug("Unable to update partial section, updating all Movie sections"); + sections.ForEach(s => UpdateSection(s.Id, settings)); + } + } + else { - _logger.Debug("Unable to update partial section, updating all Movie sections"); sections.ForEach(s => UpdateSection(s.Id, settings)); } } - else - { - sections.ForEach(s => UpdateSection(s.Id, settings)); - } _logger.Debug("Finished sending Update Request to Plex Server (took {0} ms)", watch.ElapsedMilliseconds); } @@ -119,6 +143,31 @@ namespace NzbDrone.Core.Notifications.Plex.Server return false; } + private bool PathUpdatesAllowed(PlexServerSettings settings, Version version) + { + try + { + if (version >= new Version(1, 20, 0, 3125)) + { + var preferences = GetPreferences(settings); + var partialScanPreference = preferences.SingleOrDefault(p => p.Id.Equals("FSEventLibraryPartialScanEnabled")); + + if (partialScanPreference == null) + { + return false; + } + + return Convert.ToBoolean(partialScanPreference.Value); + } + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to check if path updates are allowed"); + } + + return false; + } + private void ValidateVersion(Version version) { if (version >= new Version(1, 3, 0) && version < new Version(1, 3, 1)) @@ -171,6 +220,34 @@ namespace NzbDrone.Core.Notifications.Plex.Server return partiallyUpdated; } + private bool UpdatePath(Movie movie, List sections, PlexServerSettings settings) + { + var pathUpdated = false; + + var movieLocation = new OsPath(movie.Path); + var mappedPath = movieLocation; + + if (settings.MapTo.IsNotNullOrWhiteSpace()) + { + mappedPath = new OsPath(settings.MapTo) + (movieLocation - new OsPath(settings.MapFrom)); + + _logger.Trace("Mapping Path from {0} to {1} for partial scan", movieLocation, mappedPath); + } + + var matchingSection = sections.FirstOrDefault(section => section.Locations.Any(location => location.Path.IsParentPath(mappedPath.FullPath))); + + if (matchingSection != null) + { + _logger.Debug("Updating Path on Plex host: {0}, Section: {1}, Path: {2}", settings.Host, matchingSection.Id, mappedPath); + + _plexServerProxy.UpdatePath(mappedPath.FullPath, matchingSection.Id, settings); + + pathUpdated = true; + } + + return pathUpdated; + } + private string GetMetadataId(int sectionId, Movie movie, string language, PlexServerSettings settings) { _logger.Debug("Getting metadata from Plex host: {0} for movie: {1}", settings.Host, movie); diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs index ca959d298..aba5eff41 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs @@ -1,4 +1,5 @@ using FluentValidation; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -11,6 +12,8 @@ namespace NzbDrone.Core.Notifications.Plex.Server { RuleFor(c => c.Host).ValidHost(); RuleFor(c => c.Port).InclusiveBetween(1, 65535); + RuleFor(c => c.MapFrom).NotEmpty().Unless(c => c.MapTo.IsNullOrWhiteSpace()); + RuleFor(c => c.MapTo).NotEmpty().Unless(c => c.MapFrom.IsNullOrWhiteSpace()); } } @@ -43,6 +46,12 @@ namespace NzbDrone.Core.Notifications.Plex.Server [FieldDefinition(5, Label = "Update Library", Type = FieldType.Checkbox)] public bool UpdateLibrary { get; set; } + [FieldDefinition(6, Label = "Map Paths From", Type = FieldType.Textbox, Advanced = true, HelpText = "Radarr Path, Used to modify movie paths when Plex sees library path location differently from Radarr")] + public string MapFrom { get; set; } + + [FieldDefinition(7, Label = "Map Paths To", Type = FieldType.Textbox, Advanced = true, HelpText = "Plex Path, Used to modify movie paths when Plex sees library path location differently from Radarr")] + public string MapTo { get; set; } + public bool IsValid => !string.IsNullOrWhiteSpace(Host); public NzbDroneValidationResult Validate()