aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs5
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs4
-rw-r--r--Jellyfin.Server.Implementations/Item/ItemCountService.cs31
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs4
-rw-r--r--MediaBrowser.Controller/Persistence/IItemCountService.cs4
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs7
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs72
8 files changed, 108 insertions, 21 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 2462a754ae..a2d3e14439 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -192,7 +192,7 @@ namespace Emby.Server.Implementations.Dto
var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
if (folderIds.Count > 0)
{
- childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
+ childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user);
}
}
@@ -700,7 +700,8 @@ namespace Emby.Server.Implementations.Dto
return count;
}
- // Fall back to individual query for special cases (Series, Season, etc.)
+ // Only reached when no batch was computed: the batch holds an entry for every folder it
+ // was asked about, zero included.
return folder.GetChildCount(user);
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index dd8c883684..71e2129ff8 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1745,9 +1745,9 @@ namespace Emby.Server.Implementations.Library
return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
}
- public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
+ public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user)
{
- return _countService.GetChildCountBatch(parentIds, userId);
+ return _countService.GetChildCountBatch(parentIds, user);
}
/// <inheritdoc/>
diff --git a/Jellyfin.Server.Implementations/Item/ItemCountService.cs b/Jellyfin.Server.Implementations/Item/ItemCountService.cs
index c42b5f9581..14b120363f 100644
--- a/Jellyfin.Server.Implementations/Item/ItemCountService.cs
+++ b/Jellyfin.Server.Implementations/Item/ItemCountService.cs
@@ -319,7 +319,7 @@ public class ItemCountService : IItemCountService
}
/// <inheritdoc/>
- public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
+ public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user)
{
ArgumentNullException.ThrowIfNull(parentIds);
@@ -332,20 +332,32 @@ public class ItemCountService : IItemCountService
var parentIdsArray = parentIds.ToArray();
+ var includeVirtual = user is null || user.DisplayMissingEpisodes;
+
var hierarchicalCounts = dbContext.BaseItems
- .Where(b => b.ParentId.HasValue)
+ .Where(b => b.ParentId.HasValue && !b.SeasonId.HasValue && (includeVirtual || !b.IsVirtualItem))
.WhereOneOrMany(parentIdsArray, b => b.ParentId!.Value)
.GroupBy(b => b.ParentId!.Value)
.Select(g => new { ParentId = g.Key, Count = g.Count() })
.ToDictionary(x => x.ParentId, x => x.Count);
+ // An episode is a child of its season even when it is not stored under one: with a flat
+ // structure ParentId points at the series, so counting by ParentId alone leaves the season
+ // empty and counts its episodes towards the series instead.
+ var seasonCounts = dbContext.BaseItems
+ .Where(b => b.SeasonId.HasValue && (includeVirtual || !b.IsVirtualItem))
+ .WhereOneOrMany(parentIdsArray, b => b.SeasonId!.Value)
+ .GroupBy(b => b.SeasonId!.Value)
+ .Select(g => new { SeasonId = g.Key, Count = g.Count() })
+ .ToDictionary(x => x.SeasonId, x => x.Count);
+
var linkedCounts = dbContext.LinkedChildren
.WhereOneOrMany(parentIdsArray, lc => lc.ParentId)
.GroupBy(lc => lc.ParentId)
.Select(g => new { ParentId = g.Key, Count = g.Count() })
.ToDictionary(x => x.ParentId, x => x.Count);
- var mergedChildCounts = GetMergedChildCounts(dbContext, parentIdsArray);
+ var mergedChildCounts = GetMergedChildCounts(dbContext, parentIdsArray, includeVirtual);
var result = new Dictionary<Guid, int>();
foreach (var parentId in parentIds)
@@ -356,7 +368,8 @@ public class ItemCountService : IItemCountService
continue;
}
- var hierarchicalCount = hierarchicalCounts.GetValueOrDefault(parentId, 0);
+ var hierarchicalCount = hierarchicalCounts.GetValueOrDefault(parentId, 0)
+ + seasonCounts.GetValueOrDefault(parentId, 0);
var linkedCount = linkedCounts.GetValueOrDefault(parentId, 0);
result[parentId] = linkedCount > 0 ? linkedCount : hierarchicalCount;
@@ -365,7 +378,7 @@ public class ItemCountService : IItemCountService
return result;
}
- private static Dictionary<Guid, int> GetMergedChildCounts(JellyfinDbContext dbContext, IReadOnlyList<Guid> parentIds)
+ private static Dictionary<Guid, int> GetMergedChildCounts(JellyfinDbContext dbContext, IReadOnlyList<Guid> parentIds, bool includeVirtual)
{
var mergedGroups = GetPresentationKeyGroups(dbContext, parentIds)
.Where(group => group.Value.Count > 1)
@@ -380,10 +393,16 @@ public class ItemCountService : IItemCountService
var memberIds = mergedGroups.SelectMany(group => group.Value).Distinct().ToArray();
var children = dbContext.BaseItems
.AsNoTracking()
- .Where(b => b.ParentId.HasValue)
+ .Where(b => b.ParentId.HasValue && !b.SeasonId.HasValue && (includeVirtual || !b.IsVirtualItem))
.WhereOneOrMany(memberIds, b => b.ParentId!.Value)
.Select(b => new { ParentId = b.ParentId!.Value, b.Id, b.PresentationUniqueKey })
.ToArray()
+ .Concat(dbContext.BaseItems
+ .AsNoTracking()
+ .Where(b => b.SeasonId.HasValue && (includeVirtual || !b.IsVirtualItem))
+ .WhereOneOrMany(memberIds, b => b.SeasonId!.Value)
+ .Select(b => new { ParentId = b.SeasonId!.Value, b.Id, b.PresentationUniqueKey })
+ .ToArray())
.GroupBy(b => b.ParentId)
.ToDictionary(
g => g.Key,
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 2a6ea214b8..c8cca1fa93 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -758,9 +758,9 @@ namespace MediaBrowser.Controller.Library
/// Returns the count of immediate children (non-recursive) for each parent.
/// </summary>
/// <param name="parentIds">The list of parent folder IDs.</param>
- /// <param name="userId">The user ID for access filtering.</param>
+ /// <param name="user">The user the counts are for, or null to count without a user's preferences.</param>
/// <returns>Dictionary mapping parent ID to child count.</returns>
- Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId);
+ Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user);
/// <summary>
/// Batch-fetches played and total counts for multiple folder items.
diff --git a/MediaBrowser.Controller/Persistence/IItemCountService.cs b/MediaBrowser.Controller/Persistence/IItemCountService.cs
index d57f1fc893..8ddf93e3e0 100644
--- a/MediaBrowser.Controller/Persistence/IItemCountService.cs
+++ b/MediaBrowser.Controller/Persistence/IItemCountService.cs
@@ -80,7 +80,7 @@ public interface IItemCountService
/// Batch-fetches child counts for multiple parent folders.
/// </summary>
/// <param name="parentIds">The list of parent folder IDs.</param>
- /// <param name="userId">The user ID for access filtering.</param>
+ /// <param name="user">The user the counts are for, or null to count without a user's preferences.</param>
/// <returns>Dictionary mapping parent ID to child count.</returns>
- Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId);
+ Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user);
}
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 803fab538f..b350f482c3 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -364,7 +364,7 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
foreach (var episode in episodes)
{
var season = seasons.FirstOrDefault(i => i.IndexNumber == episode.ParentIndexNumber);
- if (season is null || (episode.SeasonId.Equals(season.Id) && episode.ParentId.Equals(season.Id)))
+ if (season is null || episode.SeasonId.Equals(season.Id))
{
continue;
}
@@ -372,11 +372,6 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
// Assign the correct season id and name to episode.
episode.SeasonId = season.Id;
episode.SeasonName = season.Name;
-
- // We need to set ParentId here for episodes in virtual seasons (e.g., flat structures), otherwise it retains the
- // ParentId from the series.
- episode.SetParent(season);
-
await episode.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs
index bdac59c013..679e6d17e3 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs
@@ -154,7 +154,7 @@ public class DtoServiceTests
.Setup(x => x.GetPlayedAndTotalCountBatch(It.IsAny<IReadOnlyList<Guid>>(), user))
.Returns(new Dictionary<Guid, (int Played, int Total)> { [season.Id] = (playedCount, totalCount) });
_libraryManagerMock
- .Setup(x => x.GetChildCountBatch(It.IsAny<IReadOnlyList<Guid>>(), It.IsAny<Guid?>()))
+ .Setup(x => x.GetChildCountBatch(It.IsAny<IReadOnlyList<Guid>>(), It.IsAny<User?>()))
.Returns(new Dictionary<Guid, int> { [season.Id] = childCount });
return (season, user);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs
index 947cf54d85..fea743f08e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Item/ItemCountServiceTests.cs
@@ -198,6 +198,78 @@ public sealed class ItemCountServiceTests : IDisposable
Assert.Equal(2, result[seriesB]);
}
+ [Fact]
+ public void GetChildCountBatch_FlatSeriesStructure_CountsEpisodesUnderTheirSeason()
+ {
+ var (seriesId, seasonId) = SeedSeries(flat: true, virtualEpisodes: false);
+
+ var result = _service.GetChildCountBatch([seriesId, seasonId], null);
+
+ Assert.Equal(2, result[seasonId]);
+
+ // The series holds the season, not the episodes: counting those here would double them up.
+ Assert.Equal(1, result[seriesId]);
+ }
+
+ [Fact]
+ public void GetChildCountBatch_SeasonFolderStructure_CountsEachEpisodeOnce()
+ {
+ var (seriesId, seasonId) = SeedSeries(flat: false, virtualEpisodes: false);
+
+ var result = _service.GetChildCountBatch([seriesId, seasonId], null);
+
+ Assert.Equal(2, result[seasonId]);
+ Assert.Equal(1, result[seriesId]);
+ }
+
+ [Fact]
+ public void GetChildCountBatch_MissingEpisodes_CountedUnlessTheUserHidesThem()
+ {
+ var (_, seasonId) = SeedSeries(flat: false, virtualEpisodes: true);
+ var user = new User("count-test", "provider", "reset");
+
+ user.DisplayMissingEpisodes = true;
+ Assert.Equal(2, _service.GetChildCountBatch([seasonId], user)[seasonId]);
+
+ // Nothing this user can open, so nothing to report.
+ user.DisplayMissingEpisodes = false;
+ Assert.Equal(0, _service.GetChildCountBatch([seasonId], user)[seasonId]);
+ }
+
+ [Fact]
+ public void GetChildCountBatch_NoUser_CountsMissingEpisodes()
+ {
+ var (_, seasonId) = SeedSeries(flat: false, virtualEpisodes: true);
+
+ Assert.Equal(2, _service.GetChildCountBatch([seasonId], null)[seasonId]);
+ }
+
+ private (Guid SeriesId, Guid SeasonId) SeedSeries(bool flat, bool virtualEpisodes)
+ {
+ var seriesId = Guid.NewGuid();
+ var seasonId = Guid.NewGuid();
+
+ using var context = CreateDbContext();
+ context.BaseItems.Add(CreateItem(seriesId));
+ context.BaseItems.Add(CreateItem(seasonId, seriesId));
+
+ // Flat: the episodes sit in the series folder, so ParentId points at the series and only
+ // SeasonId ties them to the season they belong to.
+ for (var i = 0; i < 2; i++)
+ {
+ var episode = CreateItem(Guid.NewGuid(), flat ? seriesId : seasonId);
+ episode.Type = "MediaBrowser.Controller.Entities.TV.Episode";
+ episode.IsFolder = false;
+ episode.IsVirtualItem = virtualEpisodes;
+ episode.SeasonId = seasonId;
+ context.BaseItems.Add(episode);
+ }
+
+ context.SaveChanges();
+
+ return (seriesId, seasonId);
+ }
+
private (User User, Guid SeriesA, Guid SeriesB) SeedMergedSeries(out Guid playedLeafId)
{
var user = new User("count-test", "provider", "reset");