diff --git a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs new file mode 100644 index 0000000000..94d7f3cd64 --- /dev/null +++ b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library +{ + /// + /// Test. + /// + public class ImageFetcherPostScanTask : ILibraryPostScanTask + { + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly ILogger _logger; + private readonly SemaphoreSlim _imageFetcherLock; + + private ConcurrentDictionary _queuedItems; + + /// + /// Initializes a new instance of the class. + /// + /// Some stuff. + public ImageFetcherPostScanTask( + ILibraryManager libraryManager, + IProviderManager providerManager, + ILogger logger) + { + _libraryManager = libraryManager; + _providerManager = providerManager; + _logger = logger; + _queuedItems = new ConcurrentDictionary(); + _imageFetcherLock = new SemaphoreSlim(1, 1); + _libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated; + _libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated; + _providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted; + } + + /// + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + // Sometimes a library scan will cause this to run twice if there's an item refresh going on. + await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var now = DateTime.UtcNow; + var itemGuids = _queuedItems.Keys.ToList(); + + for (var i = 0; i < itemGuids.Count; i++) + { + if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem)) + { + continue; + } + + _logger.LogDebug( + "Updating remote images for item {ItemId} with media type {ItemMediaType}", + queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture), + queuedItem.item.GetType()); + await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); + + _queuedItems.TryRemove(queuedItem.item.Id, out _); + } + + if (itemGuids.Count > 0) + { + _logger.LogInformation( + "Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.", + itemGuids.Count.ToString(CultureInfo.InvariantCulture), + (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + else + { + _logger.LogDebug("No images were updated."); + } + } + finally + { + _imageFetcherLock.Release(); + } + } + + private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs) + { + if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0) + { + _queuedItems.AddOrUpdate( + itemChangeEventArgs.Item.Id, + (itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason), + (key, existingValue) => existingValue); + } + } + + private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs e) + { + if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0) + { + _queuedItems.AddOrUpdate( + e.Argument.Id, + (e.Argument, ItemUpdateType.None), + (key, existingValue) => existingValue); + } + + // The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on + // the item that was refreshed regardless of children refreshes. So we take it as a signal + // that the refresh is entirely completed. + Run(null, CancellationToken.None).GetAwaiter().GetResult(); + } + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 00282b71a5..74788a320f 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -857,7 +857,21 @@ namespace Emby.Server.Implementations.Library /// Task{Person}. public Person GetPerson(string name) { - return CreateItemByName(Person.GetPath, name, new DtoOptions(true)); + var path = Person.GetPath(name); + var id = GetItemByNameId(path); + if (!(GetItemById(id) is Person item)) + { + item = new Person + { + Name = name, + Id = id, + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + Path = path + }; + } + + return item; } /// @@ -1940,19 +1954,9 @@ namespace Emby.Server.Implementations.Library } /// - public async Task UpdateItemsAsync(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + public Task UpdateItemsAsync(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - foreach (var item in items) - { - if (item.IsFileProtocol) - { - ProviderManager.SaveMetadata(item, updateReason); - } - - item.DateLastSaved = DateTime.UtcNow; - - await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); - } + RunMetadataSavers(items, updateReason); _itemRepository.SaveItems(items, cancellationToken); @@ -1983,12 +1987,27 @@ namespace Emby.Server.Implementations.Library } } } + + return Task.CompletedTask; } /// public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); + public void RunMetadataSavers(IReadOnlyList items, ItemUpdateType updateReason) + { + foreach (var item in items) + { + if (item.IsFileProtocol) + { + ProviderManager.SaveMetadata(item, updateReason); + } + + item.DateLastSaved = DateTime.UtcNow; + } + } + /// /// Reports the item removed. /// diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 68126bd8a8..0e99646084 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1433,7 +1433,6 @@ namespace MediaBrowser.Controller.Entities new List(); var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); - await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh if (ownedItemsChanged) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 11542c1cad..b8737b48cd 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -352,11 +352,6 @@ namespace MediaBrowser.Controller.Entities { await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } - else - { - // metadata is up-to-date; make sure DB has correct images dimensions and hash - await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false); - } continue; } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 804170d5c9..d329495b9c 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -567,5 +567,7 @@ namespace MediaBrowser.Controller.Library void AddExternalSubtitleStreams(List streams, string videoPath, string[] files); + + void RunMetadataSavers(IReadOnlyList items, ItemUpdateType updateReason); } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index f110eafa5a..7ca51f4b5a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -231,14 +231,14 @@ namespace MediaBrowser.Providers.Manager private async Task SavePeopleMetadataAsync(List people, LibraryOptions libraryOptions, CancellationToken cancellationToken) { + var personsToSave = new List(); + foreach (var person in people) { cancellationToken.ThrowIfCancellationRequested(); if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl)) { - var updateType = ItemUpdateType.MetadataDownload; - var saveEntity = false; var personEntity = LibraryManager.GetPerson(person.Name); foreach (var id in person.ProviderIds) @@ -255,15 +255,17 @@ namespace MediaBrowser.Providers.Manager await AddPersonImageAsync(personEntity, libraryOptions, person.ImageUrl, cancellationToken).ConfigureAwait(false); saveEntity = true; - updateType |= ItemUpdateType.ImageUpdate; } if (saveEntity) { - await personEntity.UpdateToRepositoryAsync(updateType, cancellationToken).ConfigureAwait(false); + personsToSave.Add(personEntity); } } } + + LibraryManager.RunMetadataSavers(personsToSave, ItemUpdateType.MetadataDownload); + LibraryManager.CreateItems(personsToSave, null, CancellationToken.None); } private async Task AddPersonImageAsync(Person personEntity, LibraryOptions libraryOptions, string imageUrl, CancellationToken cancellationToken)