From 24886d48494ab579c860654a908dbbe7fa5b8525 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 19 Jun 2026 11:24:27 -0400 Subject: Remove orphaned people --- .../ScheduledTasks/Tasks/PeopleValidationTask.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) (limited to 'Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs') diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 6e4e5c7808..abe75c8d67 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks.Tasks; @@ -20,6 +21,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -27,11 +29,13 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory dbContextFactory) + /// Instance of the interface. + public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory dbContextFactory, ILogger logger) { _libraryManager = libraryManager; _localization = localization; _dbContextFactory = dbContextFactory; + _logger = logger; } /// @@ -71,13 +75,13 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - IProgress subProgress = new Progress((val) => progress.Report(val / 2)); + IProgress subProgress = new Progress((val) => progress.Report(val / 3)); await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false); - subProgress = new Progress((val) => progress.Report((val / 2) + 50)); var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { + subProgress = new Progress((val) => progress.Report((val / 3) + 33)); var dupQuery = context.Peoples .GroupBy(e => new { e.Name, e.PersonType }) .Where(e => e.Count() > 1) @@ -124,6 +128,13 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask } subProgress.Report(100); + var peopleToDelete = await context.Peoples + .Where(p => !context.PeopleBaseItemMap.Any(m => m.PeopleId.Equals(p.Id))) + .ExecuteDeleteAsync(cancellationToken) + .ConfigureAwait(false); + _logger.LogInformation("Removed {Count} orphaned people.", peopleToDelete); + + progress.Report(100); } } } -- cgit v1.2.3 From 310a47c1d4f241346cc4cda4e025758bf1e6247c Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 19 Jun 2026 23:10:32 -0400 Subject: Reorder ValidatePeople --- .../ScheduledTasks/Tasks/PeopleValidationTask.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs') diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index abe75c8d67..96483ced99 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -75,13 +75,10 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - IProgress subProgress = new Progress((val) => progress.Report(val / 3)); - await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false); - var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { - subProgress = new Progress((val) => progress.Report((val / 3) + 33)); + IProgress subProgress = new Progress((val) => progress.Report(val / 2)); var dupQuery = context.Peoples .GroupBy(e => new { e.Name, e.PersonType }) .Where(e => e.Count() > 1) @@ -127,14 +124,18 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask ArrayPool.Shared.Return(buffer); } - subProgress.Report(100); var peopleToDelete = await context.Peoples .Where(p => !context.PeopleBaseItemMap.Any(m => m.PeopleId.Equals(p.Id))) .ExecuteDeleteAsync(cancellationToken) .ConfigureAwait(false); _logger.LogInformation("Removed {Count} orphaned people.", peopleToDelete); - progress.Report(100); + subProgress.Report(100); } + + IProgress validateProgress = new Progress((val) => progress.Report((val / 2) + 50)); + await _libraryManager.ValidatePeopleAsync(validateProgress, cancellationToken).ConfigureAwait(false); + + progress.Report(100); } } -- cgit v1.2.3 From 1947296edd449e9a7244d18716fcb8ff0e9f0dc8 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 25 Jun 2026 19:32:36 +0200 Subject: Don't run heavy DB tasks while scan is running --- .../ScheduledTasks/Tasks/OptimizeDatabaseTask.cs | 16 +++++++++++++++- .../ScheduledTasks/Tasks/PeopleValidationTask.cs | 7 +++++++ 2 files changed, 22 insertions(+), 1 deletion(-) (limited to 'Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs') diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index 92d7a3907a..8d133dc074 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -17,6 +18,7 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask private readonly ILogger _logger; private readonly ILocalizationManager _localization; private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider; + private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. @@ -24,14 +26,17 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask /// Instance of the interface. /// Instance of the interface. /// Instance of the JellyfinDatabaseProvider that can be used for provider specific operations. + /// Instance of the interface. public OptimizeDatabaseTask( ILogger logger, ILocalizationManager localization, - IJellyfinDatabaseProvider jellyfinDatabaseProvider) + IJellyfinDatabaseProvider jellyfinDatabaseProvider, + ILibraryManager libraryManager) { _logger = logger; _localization = localization; _jellyfinDatabaseProvider = jellyfinDatabaseProvider; + _libraryManager = libraryManager; } /// @@ -68,6 +73,15 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { + // Vacuuming/checkpointing requires an exclusive lock on the database. Running it while a library scan is in + // progress causes both operations to contend for the database and can stall the scan, so defer optimization + // until no scan is running. The task will run again on its next trigger. + if (_libraryManager.IsScanRunning) + { + _logger.LogInformation("Skipping database optimization because a library scan is currently running."); + return; + } + _logger.LogInformation("Optimizing and vacuuming jellyfin.db..."); try diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 6e4e5c7808..2a38b8c446 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -71,6 +71,13 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { + // People validation performs heavy database writes that contend with an active library scan. + // Defer it until the scan has finished; the task will run again on its next trigger. + if (_libraryManager.IsScanRunning) + { + return; + } + IProgress subProgress = new Progress((val) => progress.Report(val / 2)); await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false); -- cgit v1.2.3 From fa07a3abe89b6e0eb96a9f8d8a3eb57dea20ca2a Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Fri, 26 Jun 2026 07:34:19 +0200 Subject: Skip backups whens can is running --- .../ScheduledTasks/Tasks/PeopleValidationTask.cs | 7 ++++++- .../FullSystemBackup/BackupService.cs | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) (limited to 'Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs') diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 2a38b8c446..305f98790d 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks.Tasks; @@ -20,6 +21,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -27,11 +29,13 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory dbContextFactory) + /// Instance of the interface. + public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory dbContextFactory, ILogger logger) { _libraryManager = libraryManager; _localization = localization; _dbContextFactory = dbContextFactory; + _logger = logger; } /// @@ -75,6 +79,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask // Defer it until the scan has finished; the task will run again on its next trigger. if (_libraryManager.IsScanRunning) { + _logger.LogInformation("Skipping people validation because a library scan is currently running."); return; } diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index a6dc5458ee..a534fa5fa0 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -12,6 +12,7 @@ using Jellyfin.Database.Implementations; using Jellyfin.Server.Implementations.StorageHelpers; using Jellyfin.Server.Implementations.SystemBackupService; using MediaBrowser.Controller; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.SystemBackupService; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -33,6 +34,7 @@ public class BackupService : IBackupService private readonly IServerApplicationPaths _applicationPaths; private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider; private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly ILibraryManager _libraryManager; private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults.General) { AllowTrailingCommas = true, @@ -50,13 +52,15 @@ public class BackupService : IBackupService /// The application paths. /// The Jellyfin database Provider in use. /// The SystemManager. + /// Instance of the interface. public BackupService( ILogger logger, IDbContextFactory dbProvider, IServerApplicationHost applicationHost, IServerApplicationPaths applicationPaths, IJellyfinDatabaseProvider jellyfinDatabaseProvider, - IHostApplicationLifetime applicationLifetime) + IHostApplicationLifetime applicationLifetime, + ILibraryManager libraryManager) { _logger = logger; _dbProvider = dbProvider; @@ -64,6 +68,7 @@ public class BackupService : IBackupService _applicationPaths = applicationPaths; _jellyfinDatabaseProvider = jellyfinDatabaseProvider; _hostApplicationLifetime = applicationLifetime; + _libraryManager = libraryManager; } /// @@ -263,6 +268,14 @@ public class BackupService : IBackupService /// public async Task CreateBackupAsync(BackupOptionsDto backupOptions) { + // Creating a backup runs a database optimization and reads the entire database under a transaction, both of + // which heavily contend with an active library scan and could capture an inconsistent database state. + if (_libraryManager.IsScanRunning) + { + _logger.LogWarning("Cannot create a backup while a library scan is running."); + throw new InvalidOperationException("Cannot create a backup while a library scan is running. Please try again once the scan has finished."); + } + var manifest = new BackupManifest() { DateCreated = DateTime.UtcNow, -- cgit v1.2.3 From dc92e3b0e489b7d6efe9ba950cbf124b84a3a4ce Mon Sep 17 00:00:00 2001 From: John Corser Date: Sun, 19 Apr 2026 13:33:29 -0400 Subject: 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 --- .../ScheduledTasks/Tasks/PeopleValidationTask.cs | 79 +++++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) (limited to 'Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs') 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); + } } -- cgit v1.2.3 From d55f80842391e1b69db927ae8938c0455ddb5192 Mon Sep 17 00:00:00 2001 From: John Corser Date: Sat, 9 May 2026 14:11:16 -0400 Subject: Move people filtering to database query Instead of loading all people names and checking each one in memory, query the database directly for Person items that need refresh: - Missing primary image OR missing overview - Not refreshed within the last 30 days This reduces the operation from N+1 queries (1 for all names + 1 per person to load) to a single filtered query returning only the IDs that need work. --- .../ScheduledTasks/Tasks/PeopleValidationTask.cs | 84 ++++++++++++---------- 1 file changed, 48 insertions(+), 36 deletions(-) (limited to 'Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs') diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 0c1d004aed..7d42693b39 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -168,55 +169,66 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask private async Task RefreshPeopleImagesAsync(IProgress progress, CancellationToken cancellationToken) { - var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery()); - var numPeople = people.Count; - var numComplete = 0; - var numRefreshed = 0; + var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30); + var personTypeName = typeof(Person).FullName!; - _logger.LogDebug("Checking {Count} people for missing images", numPeople); - - foreach (var person in people) + var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) { - cancellationToken.ThrowIfCancellationRequested(); + var peopleIds = await context.BaseItems + .AsNoTracking() + .Where(b => b.Type == personTypeName) + .Where(b => b.DateLastRefreshed == null || b.DateLastRefreshed < thirtyDaysAgo) + .Where(b => + !b.Images!.Any(i => i.ImageType == ImageInfoImageType.Primary) || + string.IsNullOrEmpty(b.Overview)) + .Select(b => b.Id) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); - try + var numPeople = peopleIds.Count; + var numComplete = 0; + var numRefreshed = 0; + + _logger.LogDebug("Found {Count} people needing image/overview refresh", numPeople); + + foreach (var personId in peopleIds) { - var item = _libraryManager.GetPerson(person); - if (item is null) + cancellationToken.ThrowIfCancellationRequested(); + + try { - continue; - } + if (_libraryManager.GetItemById(personId) is not Person item) + { + continue; + } - var hasImage = item.HasImage(ImageType.Primary, 0); - var hasOverview = !string.IsNullOrWhiteSpace(item.Overview); + var hasImage = item.HasImage(ImageType.Primary, 0); + var hasOverview = !string.IsNullOrWhiteSpace(item.Overview); - if ((hasImage && hasOverview) || (DateTime.UtcNow - item.DateLastRefreshed).TotalDays < 30) + 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) { - continue; + throw; } - - var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + catch (Exception ex) { - ImageRefreshMode = hasImage ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default, - MetadataRefreshMode = hasOverview ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default - }; + _logger.LogError(ex, "Error refreshing images for person {PersonId}", personId); + } - 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); } - numComplete++; - progress.Report(100.0 * numComplete / numPeople); + _logger.LogInformation("Refreshed metadata for {Count} people missing images or overview", numRefreshed); } - - _logger.LogInformation("Refreshed metadata for {Count} people missing images or overview", numRefreshed); } } -- cgit v1.2.3 From a888257c8299026fb73c4c90160c71cdacc0ee37 Mon Sep 17 00:00:00 2001 From: John Corser Date: Sat, 9 May 2026 14:38:22 -0400 Subject: Project hasImage/hasOverview from DB query Instead of re-checking image/overview on the domain object after loading, project the values directly from the database query as part of the anonymous type selection. This avoids redundant checks since the DB already has this information. --- .../ScheduledTasks/Tasks/PeopleValidationTask.cs | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) (limited to 'Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs') diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 7d42693b39..bdda7937fd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -9,7 +9,6 @@ using Jellyfin.Database.Implementations.Entities; 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; @@ -175,41 +174,43 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { - var peopleIds = await context.BaseItems + var people = await context.BaseItems .AsNoTracking() .Where(b => b.Type == personTypeName) .Where(b => b.DateLastRefreshed == null || b.DateLastRefreshed < thirtyDaysAgo) .Where(b => !b.Images!.Any(i => i.ImageType == ImageInfoImageType.Primary) || string.IsNullOrEmpty(b.Overview)) - .Select(b => b.Id) + .Select(b => new + { + b.Id, + HasImage = b.Images!.Any(i => i.ImageType == ImageInfoImageType.Primary), + HasOverview = !string.IsNullOrEmpty(b.Overview) + }) .ToListAsync(cancellationToken) .ConfigureAwait(false); - var numPeople = peopleIds.Count; + var numPeople = people.Count; var numComplete = 0; var numRefreshed = 0; _logger.LogDebug("Found {Count} people needing image/overview refresh", numPeople); - foreach (var personId in peopleIds) + foreach (var entry in people) { cancellationToken.ThrowIfCancellationRequested(); try { - if (_libraryManager.GetItemById(personId) is not Person item) + if (_libraryManager.GetItemById(entry.Id) is not Person item) { continue; } - var hasImage = item.HasImage(ImageType.Primary, 0); - var hasOverview = !string.IsNullOrWhiteSpace(item.Overview); - var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { - ImageRefreshMode = hasImage ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default, - MetadataRefreshMode = hasOverview ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default + ImageRefreshMode = entry.HasImage ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default, + MetadataRefreshMode = entry.HasOverview ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default }; await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); @@ -221,7 +222,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask } catch (Exception ex) { - _logger.LogError(ex, "Error refreshing images for person {PersonId}", personId); + _logger.LogError(ex, "Error refreshing images for person {PersonId}", entry.Id); } numComplete++; -- cgit v1.2.3 From ef6f342a54c9c6c8713b650c4ad60005af600ade Mon Sep 17 00:00:00 2001 From: John Corser Date: Sun, 17 May 2026 10:28:40 -0400 Subject: Use IItemTypeLookup and QueryPartitionHelpers Address review feedback: - Replace typeof(Person).FullName with IItemTypeLookup.BaseItemKindNames - Replace foreach+ToListAsync with PartitionEagerAsync for batched iteration with built-in progress reporting - Check HasImage/HasOverview on the loaded domain Person object instead of projecting from the DB query --- .../ScheduledTasks/Tasks/PeopleValidationTask.cs | 102 +++++++++++++-------- 1 file changed, 65 insertions(+), 37 deletions(-) (limited to 'Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs') diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index bdda7937fd..dff9a473af 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -4,10 +4,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; @@ -27,6 +29,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask private readonly IDbContextFactory _dbContextFactory; private readonly IFileSystem _fileSystem; private readonly ILogger _logger; + private readonly IItemTypeLookup _itemTypeLookup; /// /// Initializes a new instance of the class. @@ -36,18 +39,21 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public PeopleValidationTask( ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory dbContextFactory, IFileSystem fileSystem, - ILogger logger) + ILogger logger, + IItemTypeLookup itemTypeLookup) { _libraryManager = libraryManager; _localization = localization; _dbContextFactory = dbContextFactory; _fileSystem = fileSystem; _logger = logger; + _itemTypeLookup = itemTypeLookup; } /// @@ -169,60 +175,50 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask private async Task RefreshPeopleImagesAsync(IProgress progress, CancellationToken cancellationToken) { var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30); - var personTypeName = typeof(Person).FullName!; + var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]; var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { - var people = await context.BaseItems + const int PartitionSize = 100; + + var numPeople = await context.BaseItems .AsNoTracking() .Where(b => b.Type == personTypeName) .Where(b => b.DateLastRefreshed == null || b.DateLastRefreshed < thirtyDaysAgo) .Where(b => !b.Images!.Any(i => i.ImageType == ImageInfoImageType.Primary) || string.IsNullOrEmpty(b.Overview)) - .Select(b => new - { - b.Id, - HasImage = b.Images!.Any(i => i.ImageType == ImageInfoImageType.Primary), - HasOverview = !string.IsNullOrEmpty(b.Overview) - }) - .ToListAsync(cancellationToken) + .CountAsync(cancellationToken) .ConfigureAwait(false); - var numPeople = people.Count; - var numComplete = 0; - var numRefreshed = 0; - _logger.LogDebug("Found {Count} people needing image/overview refresh", numPeople); - foreach (var entry in people) + if (numPeople == 0) { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - if (_libraryManager.GetItemById(entry.Id) is not Person item) - { - continue; - } + progress.Report(100); + return; + } - var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ImageRefreshMode = entry.HasImage ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default, - MetadataRefreshMode = entry.HasOverview ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default - }; + var numComplete = 0; + var numRefreshed = 0; - await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); - numRefreshed++; - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) + await foreach (var entry in context.BaseItems + .AsNoTracking() + .Where(b => b.Type == personTypeName) + .Where(b => b.DateLastRefreshed == null || b.DateLastRefreshed < thirtyDaysAgo) + .Where(b => + !b.Images!.Any(i => i.ImageType == ImageInfoImageType.Primary) || + string.IsNullOrEmpty(b.Overview)) + .OrderBy(b => b.Id) + .WithPartitionProgress(partition => _logger.LogDebug("Processing people partition {Partition}", partition)) + .PartitionEagerAsync(PartitionSize, cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + if (await RefreshPersonAsync(entry.Id, cancellationToken).ConfigureAwait(false)) { - _logger.LogError(ex, "Error refreshing images for person {PersonId}", entry.Id); + numRefreshed++; } numComplete++; @@ -232,4 +228,36 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask _logger.LogInformation("Refreshed metadata for {Count} people missing images or overview", numRefreshed); } } + + private async Task RefreshPersonAsync(Guid personId, CancellationToken cancellationToken) + { + try + { + if (_libraryManager.GetItemById(personId) is not Person item) + { + return false; + } + + var hasImage = item.HasImage(MediaBrowser.Model.Entities.ImageType.Primary); + var hasOverview = !string.IsNullOrEmpty(item.Overview); + + 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); + return true; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing images for person {PersonId}", personId); + return false; + } + } } -- cgit v1.2.3