aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
blob: 7d53f40ce754e19f408888b2f766b884b658ef2d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;

namespace Emby.Server.Implementations.Library.Validators;

/// <summary>
/// Class PeopleValidator.
/// </summary>
public class PeopleValidator
{
    /// <summary>
    /// The library manager.
    /// </summary>
    private readonly ILibraryManager _libraryManager;

    /// <summary>
    /// The logger.
    /// </summary>
    private readonly ILogger<PeopleValidator> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="PeopleValidator" /> class.
    /// </summary>
    /// <param name="libraryManager">The library manager.</param>
    /// <param name="logger">The logger.</param>
    public PeopleValidator(ILibraryManager libraryManager, ILogger<PeopleValidator> logger)
    {
        _libraryManager = libraryManager;
        _logger = logger;
    }

    /// <summary>
    /// Validates the people.
    /// </summary>
    /// <param name="progress">The progress.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>Task.</returns>
    public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
    {
        // Before the refresh below walks them: a credit no item maps to any more stands for nothing,
        // and while it is there the person it names cannot reach the dead-person sweep either.
        var numOrphaned = _libraryManager.DeleteOrphanedCredits();
        if (numOrphaned > 0)
        {
            _logger.LogInformation("Deleted {Amount} credits no item maps to", numOrphaned);
        }

        var names = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
        var existingPersonIds = _libraryManager.GetItemIds(new InternalItemsQuery
        {
            IncludeItemTypes = [BaseItemKind.Person]
        }).ToHashSet();

        var (newNames, deadIds) = PartitionCreditsByPersonId(names, _libraryManager.GetPersonId, existingPersonIds);

        var numComplete = 0;
        var count = names.Count;
        var refreshed = 0;

        foreach (var name in names)
        {
            cancellationToken.ThrowIfCancellationRequested();

            try
            {
                var item = _libraryManager.GetOrCreatePerson(name);
                var isNew = !existingPersonIds.Contains(item.Id);
                var neverRefreshed = item.DateLastRefreshed == default;

                if (isNew || neverRefreshed)
                {
                    await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
                    refreshed++;
                }
            }
            catch (OperationCanceledException)
            {
                // Don't clutter the log
                throw;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error refreshing {PersonName}", name);
            }

            numComplete++;
            double percent = numComplete;
            percent /= count;
            percent *= 100;

            progress.Report(percent);
        }

        _logger.LogInformation(
            "Refreshed metadata for {RefreshedCount} people out of {TotalCount} total, {NewCount} of which had no item yet",
            refreshed,
            count,
            newNames.Count);

        // A person somebody locked is theirs, not ours, however little the library still credits them.
        var deadEntities = deadIds
            .Select(_libraryManager.GetItemById)
            .OfType<Person>()
            .Where(item => !item.IsLocked)
            .ToList();

        foreach (var item in deadEntities)
        {
            _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
        }

        _libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);

        progress.Report(100);
    }

    /// <summary>
    /// Splits the person items into the ones a credit still calls for and the ones nothing does.
    /// </summary>
    /// <param name="creditNames">Every name credited on an item, from the people table.</param>
    /// <param name="getPersonId">Maps a credit name to the id its person item has.</param>
    /// <param name="existingPersonIds">The ids of the person items that exist.</param>
    /// <returns>The credits needing an item, and the ids of the items nothing credits.</returns>
    internal static (List<string> NewNames, List<Guid> DeadIds) PartitionCreditsByPersonId(
        IReadOnlyList<string> creditNames,
        Func<string, Guid> getPersonId,
        IReadOnlySet<Guid> existingPersonIds)
    {
        ArgumentNullException.ThrowIfNull(creditNames);
        ArgumentNullException.ThrowIfNull(getPersonId);
        ArgumentNullException.ThrowIfNull(existingPersonIds);

        var newNames = new List<string>();
        var liveIds = new HashSet<Guid>();

        foreach (var name in creditNames)
        {
            var personId = getPersonId(name);

            // Distinct credit names can normalize onto one id; only the first of them needs an item.
            if (liveIds.Add(personId) && !existingPersonIds.Contains(personId))
            {
                newNames.Add(name);
            }
        }

        var deadIds = existingPersonIds.Where(id => !liveIds.Contains(id)).ToList();

        return (newNames, deadIds);
    }
}