Fix played/unplayed filter for empty Series and BoxSets
This commit is contained in:
@@ -503,7 +503,7 @@ public sealed partial class BaseItemRepository
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IQueryable<Guid> GetFullyPlayedFolderIdsQuery(JellyfinDbContext context, IQueryable<Guid> folderIds, User user)
|
||||
public IQueryable<Guid> GetFoldersWithUnplayedItemsQuery(JellyfinDbContext context, IQueryable<Guid> folderIds, User user)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(folderIds);
|
||||
@@ -517,24 +517,27 @@ public sealed partial class BaseItemRepository
|
||||
.Where(b => !b.IsFolder && !b.IsVirtualItem);
|
||||
leafItems = ApplyAccessFiltering(context, leafItems, filter);
|
||||
|
||||
var playedLeafItems = leafItems
|
||||
.Select(b => new { b.Id, Played = b.UserData!.Any(ud => ud.UserId == userId && ud.Played) });
|
||||
// Only unplayed leaves are joined, so each branch is a semi-join per folder instead of a
|
||||
// played-vs-total count per folder. Folders with no leaves at all simply never match.
|
||||
var unplayedLeafItems = leafItems
|
||||
.Where(b => !b.UserData!.Any(ud => ud.UserId == userId && ud.Played))
|
||||
.Select(b => new { b.Id });
|
||||
|
||||
var ancestorLeaves = context.AncestorIds
|
||||
.Where(a => folderIds.Contains(a.ParentItemId))
|
||||
.Join(
|
||||
playedLeafItems,
|
||||
unplayedLeafItems,
|
||||
a => a.ItemId,
|
||||
b => b.Id,
|
||||
(a, b) => new { FolderId = a.ParentItemId, b.Id, b.Played });
|
||||
(a, b) => a.ParentItemId);
|
||||
|
||||
var linkedLeaves = context.LinkedChildren
|
||||
.Where(lc => folderIds.Contains(lc.ParentId))
|
||||
.Join(
|
||||
playedLeafItems,
|
||||
unplayedLeafItems,
|
||||
lc => lc.ChildId,
|
||||
b => b.Id,
|
||||
(lc, b) => new { FolderId = lc.ParentId, b.Id, b.Played });
|
||||
(lc, b) => lc.ParentId);
|
||||
|
||||
var linkedFolderLeaves = context.LinkedChildren
|
||||
.Where(lc => folderIds.Contains(lc.ParentId))
|
||||
@@ -549,16 +552,13 @@ public sealed partial class BaseItemRepository
|
||||
a => a.ParentItemId,
|
||||
(x, a) => new { x.ParentId, DescendantId = a.ItemId })
|
||||
.Join(
|
||||
playedLeafItems,
|
||||
unplayedLeafItems,
|
||||
x => x.DescendantId,
|
||||
b => b.Id,
|
||||
(x, b) => new { FolderId = x.ParentId, b.Id, b.Played });
|
||||
(x, b) => x.ParentId);
|
||||
|
||||
return ancestorLeaves
|
||||
.Union(linkedLeaves)
|
||||
.Union(linkedFolderLeaves)
|
||||
.GroupBy(x => x.FolderId)
|
||||
.Where(g => g.Select(x => x.Id).Distinct().Count() == g.Where(x => x.Played).Select(x => x.Id).Distinct().Count())
|
||||
.Select(g => g.Key);
|
||||
.Union(linkedFolderLeaves);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,19 +476,14 @@ public sealed partial class BaseItemRepository
|
||||
var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
|
||||
var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
|
||||
|
||||
// Series: played = at least one episode AND all episodes played; unplayed = otherwise.
|
||||
IQueryable<Guid> playedSeriesIds = hasSeries
|
||||
? context.BaseItems
|
||||
.AsNoTracking()
|
||||
.Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
|
||||
.GroupBy(e => e.SeriesId!.Value)
|
||||
.Where(g => !g.Any(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played)))
|
||||
.Select(g => g.Key)
|
||||
: Enumerable.Empty<Guid>().AsQueryable();
|
||||
// Series and BoxSets are matched by absence of an unplayed descendant rather than by
|
||||
// "all descendants played".
|
||||
var seriesEpisodes = context.BaseItems
|
||||
.AsNoTracking()
|
||||
.Where(e => !e.IsFolder && !e.IsVirtualItem);
|
||||
|
||||
// BoxSet: played = all children played.
|
||||
IQueryable<Guid> playedBoxSetIds = hasBoxSet
|
||||
? GetFullyPlayedFolderIdsQuery(
|
||||
IQueryable<Guid> unplayedBoxSetIds = hasBoxSet
|
||||
? GetFoldersWithUnplayedItemsQuery(
|
||||
context,
|
||||
baseQuery.Where(e => e.Type == boxSetTypeName).Select(e => e.Id),
|
||||
filter.User!)
|
||||
@@ -502,15 +497,17 @@ public sealed partial class BaseItemRepository
|
||||
if (isPlayed)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
(e.Type == seriesTypeName && playedSeriesIds.Contains(e.Id))
|
||||
|| (e.Type == boxSetTypeName && playedBoxSetIds.Contains(e.Id))
|
||||
(e.Type == seriesTypeName && !seriesEpisodes.Any(ep => ep.SeriesId == e.Id
|
||||
&& !ep.UserData!.Any(ud => ud.UserId == userId && ud.Played)))
|
||||
|| (e.Type == boxSetTypeName && !unplayedBoxSetIds.Contains(e.Id))
|
||||
|| (e.Type != seriesTypeName && e.Type != boxSetTypeName && playedItemIds.Contains(e.Id)));
|
||||
}
|
||||
else
|
||||
{
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
(e.Type == seriesTypeName && !playedSeriesIds.Contains(e.Id))
|
||||
|| (e.Type == boxSetTypeName && !playedBoxSetIds.Contains(e.Id))
|
||||
(e.Type == seriesTypeName && seriesEpisodes.Any(ep => ep.SeriesId == e.Id
|
||||
&& !ep.UserData!.Any(ud => ud.UserId == userId && ud.Played)))
|
||||
|| (e.Type == boxSetTypeName && unplayedBoxSetIds.Contains(e.Id))
|
||||
|| (e.Type != seriesTypeName && e.Type != boxSetTypeName && !playedItemIds.Contains(e.Id)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,11 +461,12 @@ namespace MediaBrowser.Controller.Entities
|
||||
var counts = libraryManager.GetPlayedAndTotalCountBatch(folderIds, user);
|
||||
var isPlayedValue = query.IsPlayed.Value;
|
||||
|
||||
return itemList.Where(i =>
|
||||
return itemList.Where(item =>
|
||||
{
|
||||
if (i.IsFolder && counts.TryGetValue(i.Id, out var c))
|
||||
if (item is Folder)
|
||||
{
|
||||
return (c.Total > 0 && c.Played == c.Total) == isPlayedValue;
|
||||
var itemCount = counts.GetValueOrDefault(item.Id);
|
||||
return (itemCount.Played >= itemCount.Total) == isPlayedValue;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -79,14 +79,14 @@ public interface IItemQueryHelpers
|
||||
Guid ancestorId);
|
||||
|
||||
/// <summary>
|
||||
/// Builds an <see cref="IQueryable{Guid}"/> of folder IDs whose descendants are all played
|
||||
/// for the given user. Composable into outer queries to avoid an extra DB roundtrip.
|
||||
/// Builds an <see cref="IQueryable{Guid}"/> of folder IDs that have at least one unplayed
|
||||
/// descendant for the given user. Composable into outer queries to avoid an extra DB roundtrip.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context the resulting query is bound to.</param>
|
||||
/// <param name="folderIds">A query yielding candidate folder IDs.</param>
|
||||
/// <param name="user">The user for access filtering and played status.</param>
|
||||
/// <returns>An <see cref="IQueryable{Guid}"/> of fully-played folder IDs.</returns>
|
||||
IQueryable<Guid> GetFullyPlayedFolderIdsQuery(
|
||||
/// <returns>An <see cref="IQueryable{Guid}"/> of folder IDs with unplayed descendants.</returns>
|
||||
IQueryable<Guid> GetFoldersWithUnplayedItemsQuery(
|
||||
JellyfinDbContext context,
|
||||
IQueryable<Guid> folderIds,
|
||||
User user);
|
||||
|
||||
Reference in New Issue
Block a user