aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs68
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs41
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs129
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/bs.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json13
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/fo.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ga.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/lb.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json14
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs9
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs2
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs6
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs24
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PeopleValidatorPartitionTests.cs117
21 files changed, 425 insertions, 118 deletions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 3db8265f6e..48b61b78a3 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -15,7 +15,6 @@ using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers;
-using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks.Tasks;
using Emby.Server.Implementations.Sorting;
@@ -35,7 +34,6 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
@@ -75,7 +73,6 @@ namespace Emby.Server.Implementations.Library
private readonly Lazy<IProviderManager> _providerManagerFactory;
private readonly Lazy<IUserViewManager> _userViewManagerFactory;
private readonly IServerApplicationHost _appHost;
- private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
private readonly IItemPersistenceService _persistenceService;
@@ -122,7 +119,6 @@ namespace Emby.Server.Implementations.Library
/// <param name="fileSystem">The file system.</param>
/// <param name="providerManagerFactory">The provider manager.</param>
/// <param name="userViewManagerFactory">The user view manager.</param>
- /// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="persistenceService">The item persistence service.</param>
/// <param name="nextUpService">The next up service.</param>
@@ -148,7 +144,6 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem,
Lazy<IProviderManager> providerManagerFactory,
Lazy<IUserViewManager> userViewManagerFactory,
- IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IItemPersistenceService persistenceService,
INextUpService nextUpService,
@@ -174,7 +169,6 @@ namespace Emby.Server.Implementations.Library
_fileSystem = fileSystem;
_providerManagerFactory = providerManagerFactory;
_userViewManagerFactory = userViewManagerFactory;
- _mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_persistenceService = persistenceService;
_nextUpService = nextUpService;
@@ -1210,6 +1204,12 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
+ public Guid GetPersonId(string name)
+ {
+ return GetItemByNameId<Person>(Person.GetPath(name));
+ }
+
+ /// <inheritdoc />
public Person? GetPerson(string name)
{
var path = Person.GetPath(name);
@@ -1222,6 +1222,33 @@ namespace Emby.Server.Implementations.Library
return null;
}
+ /// <inheritdoc />
+ public Person GetOrCreatePerson(string name)
+ {
+ var existing = GetPerson(name);
+ if (existing is not null)
+ {
+ return existing;
+ }
+
+ var path = Person.GetPath(name);
+ var info = Directory.CreateDirectory(path);
+ var item = new Person
+ {
+ Name = name,
+ Id = GetItemByNameId<Person>(path),
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc,
+ Path = path
+ };
+
+ item.PresentationUniqueKey = item.CreatePresentationUniqueKey();
+
+ CreateItem(item, null);
+
+ return item;
+ }
+
/// <summary>
/// Gets the studio.
/// </summary>
@@ -1354,15 +1381,6 @@ namespace Emby.Server.Implementations.Library
return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
}
- /// <inheritdoc />
- public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
- {
- // Ensure the location is available.
- Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
-
- return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
- }
-
/// <summary>
/// Reloads the root media folder.
/// </summary>
@@ -3746,27 +3764,14 @@ namespace Emby.Server.Implementations.Library
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
- var createEntity = false;
var personEntity = GetPerson(person.Name);
if (personEntity is null)
{
try
{
- var path = Person.GetPath(person.Name);
- var info = Directory.CreateDirectory(path);
- personEntity = new Person()
- {
- Name = person.Name,
- Id = GetItemByNameId<Person>(path),
- DateCreated = info.CreationTimeUtc,
- DateModified = info.LastWriteTimeUtc,
- Path = path
- };
-
- personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
+ personEntity = GetOrCreatePerson(person.Name);
saveEntity = true;
- createEntity = true;
}
catch (Exception ex)
{
@@ -3800,11 +3805,6 @@ namespace Emby.Server.Implementations.Library
if (saveEntity)
{
- if (createEntity)
- {
- CreateItems([personEntity], null, CancellationToken.None);
- }
-
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
personEntity.DateLastSaved = DateTime.UtcNow;
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index fa7112eb90..690466be70 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
@@ -61,6 +62,9 @@ public class ArtistsValidator
var count = names.Count;
var refreshed = 0;
+ var liveIds = new HashSet<Guid>();
+ var unresolved = 0;
+
foreach (var name in names)
{
try
@@ -73,13 +77,20 @@ public class ArtistsValidator
// Fall back to GetArtist if not found (creates new item if needed)
item ??= _libraryManager.GetArtist(name);
- var isNew = !existingArtistIds.Contains(item.Id);
- var neverRefreshed = item.DateLastRefreshed == default;
- if (isNew || neverRefreshed)
+ // A name with no item is nothing to refresh, and nothing to keep alive either.
+ if (item is not null)
{
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
- refreshed++;
+ liveIds.Add(item.Id);
+
+ var isNew = !existingArtistIds.Contains(item.Id);
+ var neverRefreshed = item.DateLastRefreshed == default;
+
+ if (isNew || neverRefreshed)
+ {
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ refreshed++;
+ }
}
}
catch (OperationCanceledException)
@@ -88,6 +99,7 @@ public class ArtistsValidator
}
catch (Exception ex)
{
+ unresolved++;
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
}
@@ -101,13 +113,26 @@ public class ArtistsValidator
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new artists out of {TotalCount} total", refreshed, count);
+ // Every name that threw is a name whose artist is missing from the live set, and deleting against
+ // a live set with holes in it deletes artists the library still refers to. Leave the sweep to a
+ // run that got a clean read of them.
+ if (unresolved > 0)
+ {
+ _logger.LogWarning(
+ "Not removing dead artists: {Count} of {TotalCount} names could not be resolved this run",
+ unresolved,
+ count);
+
+ progress.Report(100);
+ return;
+ }
+
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.MusicArtist],
- IsDeadArtist = true,
IsLocked = false
- }).Cast<MusicArtist>()
- .Where(item => item.IsAccessedByName)
+ }).OfType<MusicArtist>()
+ .Where(item => item.IsAccessedByName && !liveIds.Contains(item.Id))
.ToList();
foreach (var item in deadEntities)
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index 078a0b921d..7d53f40ce7 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -1,12 +1,12 @@
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 MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
@@ -17,112 +17,143 @@ namespace Emby.Server.Implementations.Library.Validators;
public class PeopleValidator
{
/// <summary>
- /// The _library manager.
+ /// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
- /// The _logger.
+ /// The logger.
/// </summary>
- private readonly ILogger _logger;
-
- private readonly IFileSystem _fileSystem;
+ 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>
- /// <param name="fileSystem">The file system.</param>
- public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
+ public PeopleValidator(ILibraryManager libraryManager, ILogger<PeopleValidator> logger)
{
_libraryManager = libraryManager;
_logger = logger;
- _fileSystem = fileSystem;
}
/// <summary>
/// Validates the people.
/// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
+ 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.LogDebug("Deleted {Amount} credits no item maps to", numOrphaned);
+ _logger.LogInformation("Deleted {Amount} credits no item maps to", numOrphaned);
}
- var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
-
- var numComplete = 0;
-
- var numPeople = people.Count;
+ var names = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
+ var existingPersonIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Person]
+ }).ToHashSet();
- IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
+ var (newNames, deadIds) = PartitionCreditsByPersonId(names, _libraryManager.GetPersonId, existingPersonIds);
- _logger.LogDebug("Will refresh {Amount} people", numPeople);
+ var numComplete = 0;
+ var count = names.Count;
+ var refreshed = 0;
- foreach (var person in people)
+ foreach (var name in names)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
- var item = _libraryManager.GetPerson(person);
- if (item is null)
- {
- _logger.LogWarning("Failed to get person: {Name}", person);
- continue;
- }
+ var item = _libraryManager.GetOrCreatePerson(name);
+ var isNew = !existingPersonIds.Contains(item.Id);
+ var neverRefreshed = item.DateLastRefreshed == default;
- var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ if (isNew || neverRefreshed)
{
- ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
- MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
- };
-
- await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ refreshed++;
+ }
}
catch (OperationCanceledException)
{
+ // Don't clutter the log
throw;
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error validating IBN entry {Person}", person);
+ _logger.LogError(ex, "Error refreshing {PersonName}", name);
}
- // Update progress
numComplete++;
double percent = numComplete;
- percent /= numPeople;
+ percent /= count;
+ percent *= 100;
- subProgress.Report(100 * percent);
+ progress.Report(percent);
}
- var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = [BaseItemKind.Person],
- IsDeadPerson = true,
- IsLocked = false
- });
+ _logger.LogInformation(
+ "Refreshed metadata for {RefreshedCount} people out of {TotalCount} total, {NewCount} of which had no item yet",
+ refreshed,
+ count,
+ newNames.Count);
- subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
+ // 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();
- var i = 0;
- foreach (var item in deadEntities.Chunk(500))
+ foreach (var item in deadEntities)
{
- _libraryManager.DeleteItemsUnsafeFast(item, true);
- subProgress.Report(100f / deadEntities.Count * (i++ * 100));
+ _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();
- _logger.LogInformation("People validation complete, deleted {Orphaned} orphaned credits", numOrphaned);
+ return (newNames, deadIds);
}
}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 49ebc45f06..c5b1213096 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -112,5 +112,11 @@
"NameExtraInterview": "Інтэрв'ю",
"NameExtraNumbered": "{0} {1}",
"NameExtraScene": "Сцэна",
- "NameExtraTrailer": "Трэйлер"
+ "NameExtraTrailer": "Трэйлер",
+ "NameExtraBehindTheScenes": "За кулісамі",
+ "NameExtraClip": "Кліп",
+ "NameExtraFeaturette": "Кароткаметражка",
+ "NameExtraSample": "Прыклад",
+ "NameExtraShort": "Кароткаметражка",
+ "NameExtraThemeSong": "Тэматычная песня"
}
diff --git a/Emby.Server.Implementations/Localization/Core/bs.json b/Emby.Server.Implementations/Localization/Core/bs.json
index aa7fe4eb24..5686807d9a 100644
--- a/Emby.Server.Implementations/Localization/Core/bs.json
+++ b/Emby.Server.Implementations/Localization/Core/bs.json
@@ -106,5 +106,17 @@
"TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay",
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
- "CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana."
+ "CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana.",
+ "NameExtraBehindTheScenes": "Iza kulisa",
+ "NameExtraClip": "Isječak",
+ "NameExtraDeletedScene": "Izbrišana scena",
+ "NameExtraFeaturette": "Kratki prilog",
+ "NameExtraInterview": "Intervju",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Uzorak",
+ "NameExtraScene": "Scena",
+ "NameExtraShort": "Kratko",
+ "NameExtraThemeSong": "Tema",
+ "NameExtraThemeVideo": "Tematski video",
+ "NameExtraTrailer": "Najava"
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index c0ad2c165a..610bc286b8 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -108,5 +108,16 @@
"CleanupUserDataTaskDescription": "Καθαρίζει όλα τα δεδομένα χρήστη (κατάσταση παρακολούθησης, κατάσταση αγαπημένων κ.λπ.) από πολυμέσα που δεν υπάρχουν πλέον για τουλάχιστον 90 ημέρες.",
"CleanupUserDataTask": "Εργασία εκκαθάρισης δεδομένων χρήστη",
"LyricDownloadFailureFromForItem": "Αποτυχία λήψης στίχων από {0} για {1}",
- "Original": "Πρωτότυπο"
+ "Original": "Πρωτότυπο",
+ "NameExtraBehindTheScenes": "Πίσω από τις Σκηνές",
+ "NameExtraDeletedScene": "Διεγραμμένη Σκηνή",
+ "NameExtraFeaturette": "Πρόσθετα βίντεο",
+ "NameExtraInterview": "Συνέντευξη",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Δείγμα",
+ "NameExtraScene": "Σκηνή",
+ "NameExtraShort": "Βίντεο μικρού μήκους",
+ "NameExtraThemeSong": "Θεματικό Τραγούδι",
+ "NameExtraThemeVideo": "Θεματικό Βίντεο",
+ "NameExtraTrailer": "τρέιλερ ταινίας"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index 36a248a1d1..9a453120dd 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -113,5 +113,12 @@
"NameExtraClip": "Klippi",
"NameExtraDeletedScene": "Poistettu Kohtaus",
"NameExtraFeaturette": "Lyhytelokuva",
- "NameExtraInterview": "Haastattelu"
+ "NameExtraInterview": "Haastattelu",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Näyte",
+ "NameExtraScene": "Kohtaus",
+ "NameExtraShort": "Lyhytfilmi",
+ "NameExtraThemeSong": "Tunnusmusiikki",
+ "NameExtraThemeVideo": "Tunnusvideo",
+ "NameExtraTrailer": "Traileri"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json
index 1a1c89da35..06a8c42f9c 100644
--- a/Emby.Server.Implementations/Localization/Core/fo.json
+++ b/Emby.Server.Implementations/Localization/Core/fo.json
@@ -8,7 +8,7 @@
"Books": "Bøkur",
"ChapterNameValue": "Kapittul {0}",
"Favorites": "Yndis",
- "Folders": "Mappur",
+ "Folders": "Skjáttur",
"Forced": "Kravt",
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
"HeaderFavoriteEpisodes": "Yndispartar",
@@ -119,7 +119,7 @@
"TaskMoveTrickplayImagesDescription": "Flytur verandi trickplay-fílur sambært savnsstillingunum.",
"NameExtraThemeVideo": "Eyðkenniskykmynd",
"NameExtraDeletedScene": "Úrtikin mynd",
- "NameExtraScene": "Mynd (scena)",
+ "NameExtraScene": "Mynd",
"NameExtraUnknown": "Eykatilfar",
"Original": "Upprunalig(t/ur)"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json
index 1ee606cc64..30e11d15f0 100644
--- a/Emby.Server.Implementations/Localization/Core/ga.json
+++ b/Emby.Server.Implementations/Localization/Core/ga.json
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad.",
"Original": "Bunaidh",
- "LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}"
+ "LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}",
+ "NameExtraBehindTheScenes": "Taobh thiar de na Radhairc",
+ "NameExtraClip": "Gearrthóg",
+ "NameExtraDeletedScene": "Radharc Scriosta",
+ "NameExtraFeaturette": "Mionghné",
+ "NameExtraInterview": "Agallamh",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Sampla",
+ "NameExtraScene": "Radharc",
+ "NameExtraShort": "Gearr",
+ "NameExtraThemeSong": "Amhrán Téama",
+ "NameExtraThemeVideo": "Físeán Téama",
+ "NameExtraTrailer": "Leantóir"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 442c26b30b..2d38c173f0 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana.",
"Original": "Original",
- "LyricDownloadFailureFromForItem": "Preuzimanje tekstova pjesmi od {0} za {1} nije uspjelo"
+ "LyricDownloadFailureFromForItem": "Preuzimanje tekstova pjesmi od {0} za {1} nije uspjelo",
+ "NameExtraBehindTheScenes": "Iza kulisa",
+ "NameExtraClip": "Klip",
+ "NameExtraDeletedScene": "Obrisana Scena",
+ "NameExtraFeaturette": "Promotivni video",
+ "NameExtraInterview": "Intervju",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Uzorak",
+ "NameExtraScene": "Scena",
+ "NameExtraShort": "Kratki film",
+ "NameExtraThemeSong": "Glavna Pjesma",
+ "NameExtraThemeVideo": "Tema videa",
+ "NameExtraTrailer": "Trailer"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lb.json b/Emby.Server.Implementations/Localization/Core/lb.json
index 917f26a49c..21d31c5fbf 100644
--- a/Emby.Server.Implementations/Localization/Core/lb.json
+++ b/Emby.Server.Implementations/Localization/Core/lb.json
@@ -108,5 +108,17 @@
"LyricDownloadFailureFromForItem": "Feeler beim Download vun de Songtexter vun {0} fir {1}",
"Original": "Original",
"CleanupUserDataTask": "Aufgab fir Berengege vu Benotzerdaten",
- "CleanupUserDataTaskDescription": "Läscht all Benotzerdaten (Ofspillstatus, Favoritestatus, asw.) vu Medien, déi zënter mindestens 90 Deeg net méi besteeënd sinn."
+ "CleanupUserDataTaskDescription": "Läscht all Benotzerdaten (Ofspillstatus, Favoritestatus, asw.) vu Medien, déi zënter mindestens 90 Deeg net méi besteeënd sinn.",
+ "NameExtraBehindTheScenes": "Hannert de Kulissen",
+ "NameExtraClip": "Clip",
+ "NameExtraDeletedScene": "Geläschte Scène",
+ "NameExtraFeaturette": "Featurette",
+ "NameExtraInterview": "Interview",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Beispill",
+ "NameExtraScene": "Scène",
+ "NameExtraShort": "Kuerzfilm",
+ "NameExtraThemeSong": "Theme-Lidd",
+ "NameExtraThemeVideo": "Theme-Video",
+ "NameExtraTrailer": "Bande-Annonce"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index cc4899ed50..c41cedf98a 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -100,14 +100,14 @@
"TaskAudioNormalization": "Garso normalizavimas",
"TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.",
"TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
- "TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų žodžius",
+ "TaskDownloadMissingLyrics": "Atsisiųsti trūkstamus dainų tekstus",
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.",
"TaskMoveTrickplayImages": "Pakeisti Trickplay atvaizdų vietą",
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius Trickplay failus pagal bibliotekos nustatymus.",
- "TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
+ "TaskDownloadMissingLyricsDescription": "Atsisiųsti dainų tekstus",
"CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamą būseną ir t. t.).",
- "LyricDownloadFailureFromForItem": "Nepavyko atsisiųsti dainos žodžių iš {0}, skirto {1}",
+ "LyricDownloadFailureFromForItem": "Nepavyko atsisiųsti dainos teksto iš {0}, skirto {1}",
"NameExtraBehindTheScenes": "Užkulisiuose",
"NameExtraClip": "Klipas",
"NameExtraDeletedScene": "Ištrinta scena",
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 76fa9e3cf7..52f1eecbe4 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
"CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas.",
"Original": "Oriģināls",
- "LyricDownloadFailureFromForItem": "Dziesmu vārdi nevarēja tikt lejupielādēti no {0} priekš {1}"
+ "LyricDownloadFailureFromForItem": "Dziesmu vārdi nevarēja tikt lejupielādēti no {0} priekš {1}",
+ "NameExtraBehindTheScenes": "Aiz kadra",
+ "NameExtraClip": "Klips",
+ "NameExtraDeletedScene": "Izdzēsta aina",
+ "NameExtraFeaturette": "Īsfilma",
+ "NameExtraInterview": "Intervija",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Paraugs",
+ "NameExtraScene": "Aina",
+ "NameExtraShort": "Īsfilma",
+ "NameExtraThemeSong": "Motīvu dziesma",
+ "NameExtraThemeVideo": "Tēmas video",
+ "NameExtraTrailer": "Treileris"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 752b74ec1c..735bc2c793 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -106,5 +106,15 @@
"TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.",
"TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment.",
"CleanupUserDataTaskDescription": "Sletter all brukerdata (avspillings-status, favoritter osv.) fra innhold som har vært utilgjengelig i minst 90 dager.",
- "CleanupUserDataTask": "Oppgave for opprydding av brukerdata"
+ "CleanupUserDataTask": "Oppgave for opprydding av brukerdata",
+ "NameExtraBehindTheScenes": "Bak kulissene",
+ "NameExtraDeletedScene": "Slettet scene",
+ "NameExtraFeaturette": "Presentasjonsfilm",
+ "NameExtraInterview": "Intervju",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Prøve",
+ "NameExtraScene": "Scene",
+ "NameExtraThemeSong": "Tema-låt",
+ "NameExtraThemeVideo": "Tema-video",
+ "NameExtraTrailer": "Trailer"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 358c19881f..dccec8067d 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -111,5 +111,14 @@
"Original": "Original",
"NameExtraBehindTheScenes": "În culise",
"NameExtraClip": "Clip",
- "NameExtraDeletedScene": "Scenă ștearsă"
+ "NameExtraDeletedScene": "Scenă ștearsă",
+ "NameExtraFeaturette": "Material bonus",
+ "NameExtraInterview": "Interviu",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Monstră",
+ "NameExtraScene": "Scenă",
+ "NameExtraShort": "Scurt",
+ "NameExtraThemeSong": "Audio de Fundal",
+ "NameExtraThemeVideo": "Video de Fundal",
+ "NameExtraTrailer": "Trailer"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index a1b5b714af..6ea625d66c 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Čiščenje uporabniških podatkov",
"CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo.",
"LyricDownloadFailureFromForItem": "Besedila ni bilo mogoče prenesti iz {0} za {1}",
- "Original": "Original"
+ "Original": "Original",
+ "NameExtraBehindTheScenes": "V zakulisju",
+ "NameExtraClip": "Klip",
+ "NameExtraDeletedScene": "Izbrisan prizor",
+ "NameExtraFeaturette": "Kratek dokumentarec o izdelavi filma",
+ "NameExtraInterview": "Intervju",
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraSample": "Vzorec",
+ "NameExtraScene": "Prizor",
+ "NameExtraShort": "Kratki film",
+ "NameExtraThemeSong": "Tematska Pesem",
+ "NameExtraThemeVideo": "Tematski Video",
+ "NameExtraTrailer": "Napovednik"
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index 42835d7ad0..092a621bfc 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Emby.Server.Implementations.Library.Validators;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
@@ -29,6 +30,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
private readonly IFileSystem _fileSystem;
private readonly ILogger<PeopleValidationTask> _logger;
+ private readonly ILogger<PeopleValidator> _validatorLogger;
private readonly IItemTypeLookup _itemTypeLookup;
/// <summary>
@@ -39,6 +41,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
/// <param name="dbContextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{PeopleValidationTask}"/> interface.</param>
+ /// <param name="validatorLogger">Instance of the <see cref="ILogger{PeopleValidator}"/> interface.</param>
/// <param name="itemTypeLookup">Instance of the <see cref="IItemTypeLookup"/> interface.</param>
public PeopleValidationTask(
ILibraryManager libraryManager,
@@ -46,6 +49,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
IDbContextFactory<JellyfinDbContext> dbContextFactory,
IFileSystem fileSystem,
ILogger<PeopleValidationTask> logger,
+ ILogger<PeopleValidator> validatorLogger,
IItemTypeLookup itemTypeLookup)
{
_libraryManager = libraryManager;
@@ -53,6 +57,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
_dbContextFactory = dbContextFactory;
_fileSystem = fileSystem;
_logger = logger;
+ _validatorLogger = validatorLogger;
_itemTypeLookup = itemTypeLookup;
}
@@ -165,7 +170,9 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
// 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<double> validateProgress = new Progress<double>((val) => progress.Report((val / 3) + 33));
- await _libraryManager.ValidatePeopleAsync(validateProgress, cancellationToken).ConfigureAwait(false);
+ await new PeopleValidator(_libraryManager, _validatorLogger)
+ .Run(validateProgress, cancellationToken)
+ .ConfigureAwait(false);
// Phase 3: Refresh images for people missing them (66-100%)
IProgress<double> refreshProgress = new Progress<double>((val) => progress.Report((val / 3) + 66));
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index e5df873f5b..c4851091c1 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -557,7 +557,7 @@ public class SubtitleController : BaseJellyfinApiController
if (!string.IsNullOrEmpty(fallbackFontPath))
{
var fontFile = _fileSystem.GetFiles(fallbackFontPath)
- .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+ .FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
var fileSize = fontFile?.Length;
if (fontFile is not null && fileSize is not null && fileSize > 0)
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 0e5a5047cd..eb2a3676ac 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -432,12 +432,18 @@ namespace MediaBrowser.Controller.Entities
public string? HasNoSubtitleTrackWithLanguage { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to return only items nothing names any more.
+ /// </summary>
public bool? IsDeadArtist { get; set; }
public bool? IsDeadStudio { get; set; }
public bool? IsDeadGenre { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to return only items nothing names any more.
+ /// </summary>
public bool? IsDeadPerson { get; set; }
/// <summary>
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index c8cca1fa93..9028b0d6b8 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -107,6 +107,13 @@ namespace MediaBrowser.Controller.Library
Person? GetPerson(string name);
/// <summary>
+ /// Gets a Person, creating and persisting it if no item exists for the name yet.
+ /// </summary>
+ /// <param name="name">The name of the person.</param>
+ /// <returns>The person.</returns>
+ Person GetOrCreatePerson(string name);
+
+ /// <summary>
/// Finds the by path.
/// </summary>
/// <param name="path">The path.</param>
@@ -153,15 +160,6 @@ namespace MediaBrowser.Controller.Library
Year GetYear(int value);
/// <summary>
- /// Validate and refresh the People sub-set of the IBN.
- /// The items are stored in the db but not loaded into memory until actually requested by an operation.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken);
-
- /// <summary>
/// Reloads the root media folder.
/// </summary>
/// <param name="progress">The progress.</param>
@@ -709,6 +707,14 @@ namespace MediaBrowser.Controller.Library
/// <returns><c>true</c> if ignored, <c>false</c> otherwise.</returns>
bool IgnoreFile(FileSystemMetadata file, BaseItem parent);
+ /// <summary>
+ /// Gets the id a <see cref="Person"/> item for the name would have, without looking it up
+ /// or creating it.
+ /// </summary>
+ /// <param name="name">The name of the person.</param>
+ /// <returns>The item id for the name.</returns>
+ Guid GetPersonId(string name);
+
Guid GetStudioId(string name);
Guid GetGenreId(string name);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PeopleValidatorPartitionTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PeopleValidatorPartitionTests.cs
new file mode 100644
index 0000000000..30f7bed208
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PeopleValidatorPartitionTests.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using Emby.Server.Implementations.Library.Validators;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+/// <summary>
+/// Tests for how the people validator decides which credits need a person item and which person items
+/// nothing credits any more. Keying either half on the item's name rather than its id put the two halves
+/// in a loop that created, refreshed and deleted the same people on every run, so these pin the id.
+/// </summary>
+public class PeopleValidatorPartitionTests
+{
+ // Stands in for the real item-by-name id: derived from the credit name, case-insensitively, and
+ // from nothing else. The property that matters is that it does not depend on the item's own name.
+ private static Guid PersonId(string creditName)
+ {
+#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms
+ var hash = System.Security.Cryptography.MD5.HashData(
+ System.Text.Encoding.Unicode.GetBytes(creditName.ToLowerInvariant()));
+#pragma warning restore CA5351 // Do Not Use Broken Cryptographic Algorithms
+ return new Guid(hash);
+ }
+
+ [Fact]
+ public void PartitionCreditsByPersonId_ProviderRenamedThePerson_KeepsThemAndCreatesNothing()
+ {
+ // The credit still says "AURORA"; the item it made has been renamed to "Aurora" by the provider
+ // that refreshed it. Nothing about the library changed, so nothing should be created or deleted.
+ var credits = new[] { "AURORA" };
+ var existing = new HashSet<Guid> { PersonId("AURORA") };
+
+ var (newNames, deadIds) = PeopleValidator.PartitionCreditsByPersonId(credits, PersonId, existing);
+
+ Assert.Empty(newNames);
+ Assert.Empty(deadIds);
+ }
+
+ [Theory]
+ // Every shape of rename seen in the wild on a real library.
+ [InlineData("AURORA")]
+ [InlineData("Amir AboulEla")]
+ [InlineData("Miguel Ángel Fuentes")]
+ [InlineData("a‐ha")]
+ [InlineData("윤현민")]
+ public void PartitionCreditsByPersonId_CreditWithAnItem_IsNeverBothCreatedAndDeleted(string creditName)
+ {
+ var existing = new HashSet<Guid> { PersonId(creditName) };
+
+ var (newNames, deadIds) = PeopleValidator.PartitionCreditsByPersonId([creditName], PersonId, existing);
+
+ Assert.Empty(newNames);
+ Assert.Empty(deadIds);
+ }
+
+ [Fact]
+ public void PartitionCreditsByPersonId_CreditWithNoItem_IsCreated()
+ {
+ var (newNames, deadIds) = PeopleValidator.PartitionCreditsByPersonId(
+ ["Wanted Person"],
+ PersonId,
+ new HashSet<Guid>());
+
+ Assert.Equal(["Wanted Person"], newNames);
+ Assert.Empty(deadIds);
+ }
+
+ [Fact]
+ public void PartitionCreditsByPersonId_ItemNoCreditNames_IsDead()
+ {
+ var orphan = PersonId("Nobody Credits Me");
+ var existing = new HashSet<Guid> { PersonId("Credited"), orphan };
+
+ var (newNames, deadIds) = PeopleValidator.PartitionCreditsByPersonId(["Credited"], PersonId, existing);
+
+ Assert.Empty(newNames);
+ Assert.Equal([orphan], deadIds);
+ }
+
+ [Fact]
+ public void PartitionCreditsByPersonId_CreditsNormalizingOntoOneId_CreateOneItem()
+ {
+ // "AURORA" and "Aurora" are one person as far as the item-by-name id is concerned, so exactly
+ // one of them should create the item and neither should end up dead.
+ var (newNames, deadIds) = PeopleValidator.PartitionCreditsByPersonId(
+ ["AURORA", "Aurora", "aurora"],
+ PersonId,
+ new HashSet<Guid>());
+
+ Assert.Single(newNames);
+ Assert.Empty(deadIds);
+ }
+
+ [Fact]
+ public void PartitionCreditsByPersonId_SecondRunAfterCreating_AsksForNothingFurther()
+ {
+ // The churn showed up as a run that never settled, so drive two rounds: whatever round one
+ // created must leave round two with nothing to do.
+ string[] credits = ["AURORA", "Amir AboulEla", "Miguel Ángel Fuentes"];
+ var existing = new HashSet<Guid>();
+
+ var (firstNames, firstDead) = PeopleValidator.PartitionCreditsByPersonId(credits, PersonId, existing);
+ Assert.Equal(3, firstNames.Count);
+ Assert.Empty(firstDead);
+
+ foreach (var created in firstNames)
+ {
+ existing.Add(PersonId(created));
+ }
+
+ var (secondNames, secondDead) = PeopleValidator.PartitionCreditsByPersonId(credits, PersonId, existing);
+
+ Assert.Empty(secondNames);
+ Assert.Empty(secondDead);
+ }
+}