using System; using System.Buffers; using System.Collections.Generic; using System.Linq; 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.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks.Tasks; /// /// Class PeopleValidationTask. /// 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. /// /// 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) { _libraryManager = libraryManager; _localization = localization; _dbContextFactory = dbContextFactory; _logger = logger; } /// public string Name => _localization.GetLocalizedString("TaskRefreshPeople"); /// public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription"); /// public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); /// public string Key => "RefreshPeople"; /// public bool IsHidden => false; /// public bool IsEnabled => true; /// public bool IsLogged => true; /// /// Creates the triggers that define when the task will run. /// /// An containing the default trigger infos for this task. public IEnumerable GetDefaultTriggers() { yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromDays(7).Ticks }; } /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { 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) .Select(e => e.Select(f => f.Id).ToArray()); var total = dupQuery.Count(); const int PartitionSize = 100; var iterator = 0; int itemCounter; var buffer = ArrayPool.Shared.Rent(PartitionSize)!; try { do { itemCounter = 0; await foreach (var item in dupQuery .Take(PartitionSize) .AsAsyncEnumerable() .WithCancellation(cancellationToken) .ConfigureAwait(false)) { buffer[itemCounter++] = item; } for (int i = 0; i < itemCounter; i++) { var item = buffer[i]; var reference = item[0]; var dups = item[1..]; await context.PeopleBaseItemMap.WhereOneOrMany(dups, e => e.PeopleId) .ExecuteUpdateAsync(e => e.SetProperty(f => f.PeopleId, reference), cancellationToken) .ConfigureAwait(false); await context.Peoples.Where(e => dups.Contains(e.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); subProgress.Report(100f / total * ((iterator * PartitionSize) + i)); } iterator++; } while (itemCounter == PartitionSize && !cancellationToken.IsCancellationRequested); } finally { ArrayPool.Shared.Return(buffer); } 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); subProgress.Report(100); } IProgress validateProgress = new Progress((val) => progress.Report((val / 2) + 50)); await _libraryManager.ValidatePeopleAsync(validateProgress, cancellationToken).ConfigureAwait(false); progress.Report(100); } }