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; 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; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory dbContextFactory) { _libraryManager = libraryManager; _localization = localization; _dbContextFactory = dbContextFactory; } /// 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) { IProgress subProgress = new Progress((val) => progress.Report(val / 2)); 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)) { 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); } subProgress.Report(100); } } }