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; namespace NzbDrone.Core.Notifications.Plex.Server { public interface IPlexServerService { void UpdateLibrary(Movie movie, PlexServerSettings settings); void UpdateLibrary(IEnumerable movie, PlexServerSettings settings); ValidationFailure Test(PlexServerSettings settings); } public class PlexServerService : IPlexServerService { private readonly ICached _versionCache; private readonly ICached _partialUpdateCache; private readonly ICached _pathScanCache; private readonly IPlexServerProxy _plexServerProxy; private readonly Logger _logger; public PlexServerService(ICacheManager cacheManager, IPlexServerProxy plexServerProxy, Logger logger) { _versionCache = cacheManager.GetCache(GetType(), "versionCache"); _partialUpdateCache = cacheManager.GetCache(GetType(), "partialUpdateCache"); _pathScanCache = cacheManager.GetCache(GetType(), "pathScanCache"); _plexServerProxy = plexServerProxy; _logger = logger; } public void UpdateLibrary(Movie movie, PlexServerSettings settings) { UpdateLibrary(new[] { movie }, settings); } public void UpdateLibrary(IEnumerable multipleMovies, PlexServerSettings settings) { try { _logger.Debug("Sending Update Request to Plex Server"); var watch = Stopwatch.StartNew(); var version = _versionCache.Get(settings.Host, () => GetVersion(settings), TimeSpan.FromHours(2)); ValidateVersion(version); 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)); var pathUpdated = true; if (pathScanCache) { foreach (var movie in multipleMovies) { pathUpdated &= UpdatePath(movie, sections, settings); if (!pathUpdated) { break; } } } // 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 { sections.ForEach(s => UpdateSection(s.Id, settings)); } } _logger.Debug("Finished sending Update Request to Plex Server (took {0} ms)", watch.ElapsedMilliseconds); } catch (Exception ex) { _logger.Warn(ex, "Failed to Update Plex host: " + settings.Host); throw; } } private List GetSections(PlexServerSettings settings) { _logger.Debug("Getting sections from Plex host: {0}", settings.Host); return _plexServerProxy.GetMovieSections(settings).ToList(); } private bool PartialUpdatesAllowed(PlexServerSettings settings, Version version) { try { if (version >= new Version(0, 9, 12, 0)) { 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 partial updates are allowed"); } 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)) { throw new PlexVersionException("Found version {0}, upgrade to PMS 1.3.1 to fix library updating and then restart Radarr", version); } } private Version GetVersion(PlexServerSettings settings) { _logger.Debug("Getting version from Plex host: {0}", settings.Host); var rawVersion = _plexServerProxy.Version(settings); var version = new Version(Regex.Match(rawVersion, @"^(\d+[.-]){4}").Value.Trim('.', '-')); return version; } private List GetPreferences(PlexServerSettings settings) { _logger.Debug("Getting preferences from Plex host: {0}", settings.Host); return _plexServerProxy.Preferences(settings); } private void UpdateSection(int sectionId, PlexServerSettings settings) { _logger.Debug("Updating Plex host: {0}, Section: {1}", settings.Host, sectionId); _plexServerProxy.Update(sectionId, settings); } private bool UpdatePartialSection(Movie movie, List sections, PlexServerSettings settings) { var partiallyUpdated = false; foreach (var section in sections) { var metadataId = GetMetadataId(section.Id, movie, section.Language, settings); if (metadataId.IsNotNullOrWhiteSpace()) { _logger.Debug("Updating Plex host: {0}, Section: {1}, Movie: {2}", settings.Host, section.Id, movie); _plexServerProxy.UpdateMovie(metadataId, settings); partiallyUpdated = true; } } 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); return _plexServerProxy.GetMetadataId(sectionId, movie.ImdbId, language, settings); } public ValidationFailure Test(PlexServerSettings settings) { try { _versionCache.Remove(settings.Host); _partialUpdateCache.Remove(settings.Host); var sections = GetSections(settings); if (sections.Empty()) { return new ValidationFailure("Host", "At least one Movie library is required"); } } catch (PlexAuthenticationException ex) { _logger.Error(ex, "Unable to connect to Plex Media Server"); return new ValidationFailure("AuthToken", "Invalid authentication token"); } catch (PlexException ex) { return new NzbDroneValidationFailure("Host", ex.Message); } catch (Exception ex) { _logger.Error(ex, "Unable to connect to Plex Media Server"); return new NzbDroneValidationFailure("Host", "Unable to connect to Plex Media Server") { DetailedDescription = ex.Message }; } return null; } } }