From 0d1483e2ddb43faf7904ef09fb022a4100397260 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 27 Sep 2026 16:30:50 -0400 Subject: Backport pull request #18139 from jellyfin/release-12.z Only regenerate extra names that were generated by the scan Original-merge: 25d97ac5d0c7b71a8d103b6a23fc2b487d4a2a47 Merged-by: crobibero Backported-by: Cody Robibero --- .../Library/LibraryManager.cs | 49 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) (limited to 'Emby.Server.Implementations/Library') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 4c468569bc..6f3df084c0 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3466,6 +3466,7 @@ namespace Emby.Server.Implementations.Library var extras = new List(); var typeCounters = new Dictionary(); + var generatedNames = new Dictionary>(); // Order by path so that the numbering handed out below does not depend on the // order the file system happened to list the folder in @@ -3502,10 +3503,12 @@ namespace Emby.Server.Implementations.Library extra = itemById; } - // An extra is named after its file, so the file is the source of truth. Items created - // by older versions, or renamed by a metadata provider, are corrected here; - // RefreshExtras persists the change. - if (!string.IsNullOrEmpty(name) && extra.LockedFields?.Contains(MetadataField.Name) != true) + // The name derived from the file is only a default. A name that came from anywhere else, + // such as a local metadata file, is the user's and has to survive the scan, so only a + // name this method handed out itself is renewed; RefreshExtras persists the change. + if (!string.IsNullOrEmpty(name) + && extra.LockedFields?.Contains(MetadataField.Name) != true + && (itemById is null || IsGeneratedExtraName(extra.Name, candidate))) { extra.Name = name; } @@ -3527,6 +3530,31 @@ namespace Emby.Server.Implementations.Library return null; } + + bool IsGeneratedExtraName(string currentName, ExtraCandidate candidate) + { + // The file name is what an extra was called before it was given a name of its type + if (string.Equals(currentName, candidate.Extra.Name, StringComparison.Ordinal)) + { + return true; + } + + if (!generatedNames.TryGetValue(candidate.ExtraType, out var names)) + { + // Any of the numbers of this type may have been handed out, as the order the extras + // of a type are numbered in shifts as files appear beside them or are taken away + names = new HashSet(StringComparer.Ordinal); + var count = candidates.Count(c => c.ExtraType == candidate.ExtraType); + for (var seen = 0; seen < count; seen++) + { + names.Add(GetNumberedExtraName(candidate.ExtraType, seen)); + } + + generatedNames[candidate.ExtraType] = names; + } + + return names.Contains(currentName); + } } /// @@ -3553,7 +3581,18 @@ namespace Emby.Server.Implementations.Library typeCounters.TryGetValue(candidate.ExtraType, out var seen); typeCounters[candidate.ExtraType] = seen + 1; - var typeName = _localization.GetServerLocalizedString(GetExtraTypeNameKey(candidate.ExtraType)); + return GetNumberedExtraName(candidate.ExtraType, seen); + } + + /// + /// Gets the name given to the n-th extra of a type that is named after its type. + /// + /// The extra type. + /// Number of extras of the type named before this one. + /// The name. + private string GetNumberedExtraName(ExtraType extraType, int seen) + { + var typeName = _localization.GetServerLocalizedString(GetExtraTypeNameKey(extraType)); return seen == 0 ? typeName -- cgit v1.2.3