From dc92e3b0e489b7d6efe9ba950cbf124b84a3a4ce Mon Sep 17 00:00:00 2001 From: John Corser Date: Sun, 19 Apr 2026 13:33:29 -0400 Subject: [PATCH] Fix actor images not displayed until clicked Move image refresh logic from PeopleValidator (which runs during library scans) into PeopleValidationTask (the "Refresh People" scheduled task). This keeps library scans fast while ensuring the scheduled task fetches missing images from remote providers like TMDB. People missing a Primary image or overview get refreshed with MetadataRefreshMode.Default instead of ValidationOnly, with a 30-day cooldown to avoid hammering providers for people they have no data for. Fixes jellyfin#8103 --- .../Tasks/PeopleValidationTask.cs | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 3451c458f9..0c1d004aed 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -5,8 +5,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -21,6 +25,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly IDbContextFactory _dbContextFactory; + private readonly IFileSystem _fileSystem; private readonly ILogger _logger; /// @@ -29,12 +34,19 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. /// Instance of the interface. - public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory dbContextFactory, ILogger logger) + public PeopleValidationTask( + ILibraryManager libraryManager, + ILocalizationManager localization, + IDbContextFactory dbContextFactory, + IFileSystem fileSystem, + ILogger logger) { _libraryManager = libraryManager; _localization = localization; _dbContextFactory = dbContextFactory; + _fileSystem = fileSystem; _logger = logger; } @@ -83,10 +95,11 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask return; } + // Phase 1: Deduplicate and remove orphaned people (0-33%) var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { - IProgress subProgress = new Progress((val) => progress.Report(val / 2)); + IProgress subProgress = new Progress((val) => progress.Report(val / 3)); var dupQuery = context.Peoples .GroupBy(e => new { e.Name, e.PersonType }) .Where(e => e.Count() > 1) @@ -141,9 +154,69 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask subProgress.Report(100); } - IProgress validateProgress = new Progress((val) => progress.Report((val / 2) + 50)); + // Phase 2: Validate people (33-66%). Runs after orphaned PeopleBaseItemMap entries are + // cleaned up above, so dead people are removed in a single pass instead of requiring a second run. + IProgress validateProgress = new Progress((val) => progress.Report((val / 3) + 33)); await _libraryManager.ValidatePeopleAsync(validateProgress, cancellationToken).ConfigureAwait(false); + // Phase 3: Refresh images for people missing them (66-100%) + IProgress refreshProgress = new Progress((val) => progress.Report((val / 3) + 66)); + await RefreshPeopleImagesAsync(refreshProgress, cancellationToken).ConfigureAwait(false); + progress.Report(100); } + + private async Task RefreshPeopleImagesAsync(IProgress progress, CancellationToken cancellationToken) + { + var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery()); + var numPeople = people.Count; + var numComplete = 0; + var numRefreshed = 0; + + _logger.LogDebug("Checking {Count} people for missing images", numPeople); + + foreach (var person in people) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var item = _libraryManager.GetPerson(person); + if (item is null) + { + continue; + } + + var hasImage = item.HasImage(ImageType.Primary, 0); + var hasOverview = !string.IsNullOrWhiteSpace(item.Overview); + + if ((hasImage && hasOverview) || (DateTime.UtcNow - item.DateLastRefreshed).TotalDays < 30) + { + continue; + } + + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ImageRefreshMode = hasImage ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default, + MetadataRefreshMode = hasOverview ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default + }; + + await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); + numRefreshed++; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing images for {Person}", person); + } + + numComplete++; + progress.Report(100.0 * numComplete / numPeople); + } + + _logger.LogInformation("Refreshed metadata for {Count} people missing images or overview", numRefreshed); + } }