Merge remote-tracking branch 'upstream/master' into fix-byname-queries

This commit is contained in:
Shadowghost
2026-08-02 22:12:57 +02:00
36 changed files with 2850 additions and 170 deletions
+42 -10
View File
@@ -88,7 +88,7 @@ namespace MediaBrowser.Controller.Entities
Model.Entities.ExtraType.Short
};
private static readonly char[] VersionDelimiters = ['-', '_', '.'];
private protected static readonly char[] VersionDelimiters = ['-', '_', '.'];
private string _sortName;
@@ -1543,19 +1543,33 @@ namespace MediaBrowser.Controller.Entities
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
var newExtraIds = Array.ConvertAll(extras, x => x.Id);
// An extra is owned by the version it is named after, so all of them are maintained together.
var currentExtras = LibraryManager.GetItemList(new InternalItemsQuery()
{
OwnerIds = [item.Id]
});
OwnerIds = item.GetOwnedVersionIds()
}).Where(e => e.ExtraType.HasValue).ToList();
var currentExtraIds = currentExtras.Select(e => e.Id).ToArray();
// Snapshot the persisted names before resolving, as FindExtras corrects the name on the
// items it hands back and may well hand back these very instances.
var currentExtraNames = new Dictionary<Guid, string>();
foreach (var extra in currentExtras)
{
currentExtraNames[extra.Id] = extra.Name;
}
var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
var newExtraIds = Array.ConvertAll(extras, x => x.Id);
var renamedExtraIds = extras
.Where(e => currentExtraNames.TryGetValue(e.Id, out var oldName) && !string.Equals(oldName, e.Name, StringComparison.Ordinal))
.Select(e => e.Id)
.ToHashSet();
var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh)
if (!extrasChanged && renamedExtraIds.Count == 0 && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh)
{
// The owner's dates may only have become known after its extras were created, so keep
// them in sync even when there is nothing to refresh.
@@ -1570,12 +1584,11 @@ namespace MediaBrowser.Controller.Entities
return false;
}
var ownerId = item.Id;
var tasks = extras.Select(i =>
{
var ownerId = item.GetOwnerIdForExtra(i);
var subOptions = new MetadataRefreshOptions(options);
if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty())
if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty() || renamedExtraIds.Contains(i.Id))
{
subOptions.ForceSave = true;
}
@@ -2920,6 +2933,25 @@ namespace MediaBrowser.Controller.Entities
return [Id];
}
/// <summary>
/// Gets the ids of this item and the versions of it whose extras it maintains.
/// </summary>
/// <returns>An array containing the version ids.</returns>
protected virtual Guid[] GetOwnedVersionIds()
{
return [Id];
}
/// <summary>
/// Gets the id of the version an extra belongs to.
/// </summary>
/// <param name="extra">The extra.</param>
/// <returns>The id of the owning version.</returns>
protected virtual Guid GetOwnerIdForExtra(BaseItem extra)
{
return Id;
}
/// <summary>
/// Get all extras associated with this item, sorted by <see cref="SortName"/>.
/// </summary>
+1 -9
View File
@@ -1101,15 +1101,7 @@ namespace MediaBrowser.Controller.Entities
items = ApplyNameFilter(items, query);
}
var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList();
var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager);
if (query.EnableTotalRecordCount)
{
result.TotalRecordCount = filteredItems.Count;
}
return result;
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
}
private static IEnumerable<BaseItem> ApplyNameFilter(IEnumerable<BaseItem> items, InternalItemsQuery query)
@@ -491,6 +491,13 @@ namespace MediaBrowser.Controller.Entities
}
var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
// Adjacency is defined by the order the query asked for, so it has to run after sorting but before paging.
if (!query.AdjacentTo.IsNullOrEmpty())
{
itemsArray = FilterForAdjacency(itemsArray, query.AdjacentTo.Value).ToArray();
}
var totalCount = itemsArray.Length;
if (query.Limit.HasValue && query.Limit.Value > 0)
@@ -887,26 +894,32 @@ namespace MediaBrowser.Controller.Entities
return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName);
}
public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, Guid adjacentTo)
/// <summary>
/// Trims an ordered list down to the requested item and its immediate neighbours.
/// </summary>
/// <param name="list">The items in the order the query returned them.</param>
/// <param name="adjacentTo">The id of the item to return the neighbours of.</param>
/// <returns>The previous item, the requested item and the next item, in order.</returns>
public static IEnumerable<BaseItem> FilterForAdjacency(IReadOnlyList<BaseItem> list, Guid adjacentTo)
{
var adjacentToItem = list.FirstOrDefault(i => i.Id.Equals(adjacentTo));
var index = list.IndexOf(adjacentToItem);
var previousId = Guid.Empty;
var nextId = Guid.Empty;
if (index > 0)
var index = -1;
for (var i = 0; i < list.Count; i++)
{
previousId = list[index - 1].Id;
if (list[i].Id.Equals(adjacentTo))
{
index = i;
break;
}
}
if (index < list.Count - 1)
// The item isn't part of this result set, so it has no neighbours in it either.
if (index < 0)
{
nextId = list[index + 1].Id;
return [];
}
return list.Where(i => i.Id.Equals(previousId) || i.Id.Equals(nextId) || i.Id.Equals(adjacentTo));
var start = Math.Max(index - 1, 0);
return list.Skip(start).Take(Math.Min(index + 2, list.Count) - start);
}
}
}
+74
View File
@@ -751,6 +751,80 @@ namespace MediaBrowser.Controller.Entities
.ToArray();
}
/// <inheritdoc />
protected override Guid[] GetOwnedVersionIds()
{
// Only the versions that live beside this one in the folder this scan covers. Linked
// versions are items of their own and maintain their extras themselves.
return [Id, .. LibraryManager.GetLocalAlternateVersionIds(this)];
}
/// <inheritdoc />
protected override Guid GetOwnerIdForExtra(BaseItem extra)
{
if (string.IsNullOrEmpty(extra.Path))
{
return Id;
}
var extraDirectory = System.IO.Path.GetDirectoryName(extra.Path.AsSpan());
var extraFileName = System.IO.Path.GetFileNameWithoutExtension(extra.Path.AsSpan());
var ownerId = Id;
var matchedLength = MatchedVersionNameLength(Path, extraDirectory, extraFileName);
foreach (var versionId in LibraryManager.GetLocalAlternateVersionIds(this))
{
var version = LibraryManager.GetItemById(versionId);
if (version is null)
{
continue;
}
// "Movie - [2160p]-trailer.mkv" belongs to "Movie - [2160p].mkv" rather than to the
// primary version, whose name it also starts with when the primary is plain "Movie.mkv"
var length = MatchedVersionNameLength(version.Path, extraDirectory, extraFileName);
if (length > matchedLength)
{
matchedLength = length;
ownerId = versionId;
}
}
return ownerId;
}
/// <summary>
/// Gets how much of an extra's file name is the name of the given version file, or 0 when the
/// extra is not named after it.
/// </summary>
/// <param name="versionPath">The path of the version.</param>
/// <param name="extraDirectory">The directory the extra lives in.</param>
/// <param name="extraFileName">The file name of the extra, without extension.</param>
/// <returns>The length of the match.</returns>
private static int MatchedVersionNameLength(string versionPath, ReadOnlySpan<char> extraDirectory, ReadOnlySpan<char> extraFileName)
{
if (string.IsNullOrEmpty(versionPath)
|| !System.IO.Path.GetDirectoryName(versionPath.AsSpan()).Equals(extraDirectory, StringComparison.OrdinalIgnoreCase))
{
return 0;
}
var versionFileName = System.IO.Path.GetFileNameWithoutExtension(versionPath.AsSpan());
if (versionFileName.IsEmpty || !extraFileName.StartsWith(versionFileName, StringComparison.OrdinalIgnoreCase))
{
return 0;
}
// The version name has to end where the extra's own name begins, so that a version
// named "Movie - 4K" does not claim the extras of "Movie - 4Kish"
var remainder = extraFileName[versionFileName.Length..];
return !remainder.IsEmpty && (remainder[0] == ' ' || Array.IndexOf(VersionDelimiters, remainder[0]) >= 0)
? versionFileName.Length
: 0;
}
protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
{
var primary = PrimaryVersionId.HasValue