|  | @@ -128,6 +128,10 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
	
		
			
				|  |  |          /// <value>The by reference items.</value>
 | 
	
		
			
				|  |  |          private ConcurrentDictionary<Guid, BaseItem> ByReferenceItems { get; set; }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        private IEnumerable<IMetadataSaver> _savers;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private readonly Func<IDirectoryWatchers> _directoryWatchersFactory;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  |          /// The _library items cache
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
	
		
			
				|  | @@ -167,13 +171,14 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
	
		
			
				|  |  |          /// <param name="userManager">The user manager.</param>
 | 
	
		
			
				|  |  |          /// <param name="configurationManager">The configuration manager.</param>
 | 
	
		
			
				|  |  |          /// <param name="userDataRepository">The user data repository.</param>
 | 
	
		
			
				|  |  | -        public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataRepository userDataRepository)
 | 
	
		
			
				|  |  | +        public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataRepository userDataRepository, Func<IDirectoryWatchers> directoryWatchersFactory)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              _logger = logger;
 | 
	
		
			
				|  |  |              _taskManager = taskManager;
 | 
	
		
			
				|  |  |              _userManager = userManager;
 | 
	
		
			
				|  |  |              ConfigurationManager = configurationManager;
 | 
	
		
			
				|  |  |              _userDataRepository = userDataRepository;
 | 
	
		
			
				|  |  | +            _directoryWatchersFactory = directoryWatchersFactory;
 | 
	
		
			
				|  |  |              ByReferenceItems = new ConcurrentDictionary<Guid, BaseItem>();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated;
 | 
	
	
		
			
				|  | @@ -191,13 +196,15 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
	
		
			
				|  |  |          /// <param name="itemComparers">The item comparers.</param>
 | 
	
		
			
				|  |  |          /// <param name="prescanTasks">The prescan tasks.</param>
 | 
	
		
			
				|  |  |          /// <param name="postscanTasks">The postscan tasks.</param>
 | 
	
		
			
				|  |  | +        /// <param name="savers">The savers.</param>
 | 
	
		
			
				|  |  |          public void AddParts(IEnumerable<IResolverIgnoreRule> rules,
 | 
	
		
			
				|  |  |              IEnumerable<IVirtualFolderCreator> pluginFolders,
 | 
	
		
			
				|  |  |              IEnumerable<IItemResolver> resolvers,
 | 
	
		
			
				|  |  |              IEnumerable<IIntroProvider> introProviders,
 | 
	
		
			
				|  |  |              IEnumerable<IBaseItemComparer> itemComparers,
 | 
	
		
			
				|  |  |              IEnumerable<ILibraryPrescanTask> prescanTasks,
 | 
	
		
			
				|  |  | -            IEnumerable<ILibraryPostScanTask> postscanTasks)
 | 
	
		
			
				|  |  | +            IEnumerable<ILibraryPostScanTask> postscanTasks,
 | 
	
		
			
				|  |  | +            IEnumerable<IMetadataSaver> savers)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              EntityResolutionIgnoreRules = rules;
 | 
	
		
			
				|  |  |              PluginFolderCreators = pluginFolders;
 | 
	
	
		
			
				|  | @@ -206,6 +213,7 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
	
		
			
				|  |  |              Comparers = itemComparers;
 | 
	
		
			
				|  |  |              PrescanTasks = prescanTasks;
 | 
	
		
			
				|  |  |              PostscanTasks = postscanTasks;
 | 
	
		
			
				|  |  | +            _savers = savers;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
	
		
			
				|  | @@ -326,7 +334,7 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
	
		
			
				|  |  |          /// <param name="newName">The new name.</param>
 | 
	
		
			
				|  |  |          /// <param name="cancellationToken">The cancellation token.</param>
 | 
	
		
			
				|  |  |          /// <returns>Task.</returns>
 | 
	
		
			
				|  |  | -        private Task UpdateSeasonZeroNames(string newName, CancellationToken cancellationToken)
 | 
	
		
			
				|  |  | +        private async Task UpdateSeasonZeroNames(string newName, CancellationToken cancellationToken)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              var seasons = RootFolder.RecursiveChildren
 | 
	
		
			
				|  |  |                  .OfType<Season>()
 | 
	
	
		
			
				|  | @@ -336,9 +344,16 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
	
		
			
				|  |  |              foreach (var season in seasons)
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  |                  season.Name = newName;
 | 
	
		
			
				|  |  | -            }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            return UpdateItems(seasons, cancellationToken);
 | 
	
		
			
				|  |  | +                try
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    await UpdateItem(season, ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                catch (Exception ex)
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    _logger.ErrorException("Error saving {0}", ex, season.Path);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
	
		
			
				|  | @@ -1278,33 +1293,35 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  | -        /// Updates the items.
 | 
	
		
			
				|  |  | +        /// Updates the item.
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  | -        /// <param name="items">The items.</param>
 | 
	
		
			
				|  |  | +        /// <param name="item">The item.</param>
 | 
	
		
			
				|  |  | +        /// <param name="updateReason">The update reason.</param>
 | 
	
		
			
				|  |  |          /// <param name="cancellationToken">The cancellation token.</param>
 | 
	
		
			
				|  |  |          /// <returns>Task.</returns>
 | 
	
		
			
				|  |  | -        private async Task UpdateItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
 | 
	
		
			
				|  |  | +        public async Task UpdateItem(BaseItem item, ItemUpdateType updateReason, CancellationToken cancellationToken)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  | -            var list = items.ToList();
 | 
	
		
			
				|  |  | +            await ItemRepository.SaveItem(item, cancellationToken).ConfigureAwait(false);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            await ItemRepository.SaveItems(list, cancellationToken).ConfigureAwait(false);
 | 
	
		
			
				|  |  | +            UpdateItemInLibraryCache(item);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            foreach (var item in list)
 | 
	
		
			
				|  |  | +            // If metadata was downloaded or edited, save external metadata
 | 
	
		
			
				|  |  | +            if ((updateReason & ItemUpdateType.MetadataEdit) == ItemUpdateType.MetadataEdit)
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  | -                UpdateItemInLibraryCache(item);
 | 
	
		
			
				|  |  | -                OnItemUpdated(item);
 | 
	
		
			
				|  |  | +                await SaveMetadata(item).ConfigureAwait(false);
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        /// <summary>
 | 
	
		
			
				|  |  | -        /// Updates the item.
 | 
	
		
			
				|  |  | -        /// </summary>
 | 
	
		
			
				|  |  | -        /// <param name="item">The item.</param>
 | 
	
		
			
				|  |  | -        /// <param name="cancellationToken">The cancellation token.</param>
 | 
	
		
			
				|  |  | -        /// <returns>Task.</returns>
 | 
	
		
			
				|  |  | -        public Task UpdateItem(BaseItem item, CancellationToken cancellationToken)
 | 
	
		
			
				|  |  | -        {
 | 
	
		
			
				|  |  | -            return UpdateItems(new[] { item }, cancellationToken);
 | 
	
		
			
				|  |  | +            if (ItemUpdated != null)
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                try
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    ItemUpdated(this, new ItemChangeEventArgs { Item = item });
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                catch (Exception ex)
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    _logger.ErrorException("Error in ItemUpdated event handler", ex);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
	
		
			
				|  | @@ -1337,22 +1354,38 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
	
		
			
				|  |  |              return ItemRepository.RetrieveItem(id, type);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        private readonly ConcurrentDictionary<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          /// <summary>
 | 
	
		
			
				|  |  | -        /// Called when [item updated].
 | 
	
		
			
				|  |  | +        /// Saves the metadata.
 | 
	
		
			
				|  |  |          /// </summary>
 | 
	
		
			
				|  |  |          /// <param name="item">The item.</param>
 | 
	
		
			
				|  |  |          /// <returns>Task.</returns>
 | 
	
		
			
				|  |  | -        private void OnItemUpdated(BaseItem item)
 | 
	
		
			
				|  |  | +        private async Task SaveMetadata(BaseItem item)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  | -            if (ItemUpdated != null)
 | 
	
		
			
				|  |  | +            foreach (var saver in _savers.Where(i => i.Supports(item)))
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  | +                var path = saver.GetSavePath(item);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                var semaphore = _fileLocks.GetOrAdd(path, key => new SemaphoreSlim(1, 1));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                var directoryWatchers = _directoryWatchersFactory();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                await semaphore.WaitAsync().ConfigureAwait(false);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |                  try
 | 
	
		
			
				|  |  |                  {
 | 
	
		
			
				|  |  | -                    ItemUpdated(this, new ItemChangeEventArgs { Item = item });
 | 
	
		
			
				|  |  | +                    directoryWatchers.TemporarilyIgnore(path);
 | 
	
		
			
				|  |  | +                    saver.Save(item, CancellationToken.None);
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |                  catch (Exception ex)
 | 
	
		
			
				|  |  |                  {
 | 
	
		
			
				|  |  | -                    _logger.ErrorException("Error in ItemUpdated event handler", ex);
 | 
	
		
			
				|  |  | +                    _logger.ErrorException("Error in metadata saver", ex);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                finally
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    directoryWatchers.RemoveTempIgnore(path);
 | 
	
		
			
				|  |  | +                    semaphore.Release();
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          }
 |