aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations/Item/ItemCountService.cs
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-03-07 20:12:42 +0100
committerShadowghost <Ghost_of_Stone@web.de>2026-03-07 20:12:42 +0100
commit077fa89717957f871b172ca4b2dc4a178efd3bc5 (patch)
tree1c2be0089b3c33cda1ed96bde4b76a715a845df7 /Jellyfin.Server.Implementations/Item/ItemCountService.cs
parent268f23f39ac18e783156b91b575ee6a105b6937c (diff)
Split BaseItemRepository and IItemRepository
Diffstat (limited to 'Jellyfin.Server.Implementations/Item/ItemCountService.cs')
-rw-r--r--Jellyfin.Server.Implementations/Item/ItemCountService.cs427
1 files changed, 427 insertions, 0 deletions
diff --git a/Jellyfin.Server.Implementations/Item/ItemCountService.cs b/Jellyfin.Server.Implementations/Item/ItemCountService.cs
new file mode 100644
index 0000000000..604db9f839
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/ItemCountService.cs
@@ -0,0 +1,427 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/// <summary>
+/// Provides item counting and played-status query operations.
+/// </summary>
+public class ItemCountService : IItemCountService
+{
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IItemTypeLookup _itemTypeLookup;
+ private readonly IItemQueryHelpers _queryHelpers;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemCountService"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The database context factory.</param>
+ /// <param name="itemTypeLookup">The item type lookup.</param>
+ /// <param name="queryHelpers">The shared query helpers.</param>
+ public ItemCountService(
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ IItemTypeLookup itemTypeLookup,
+ IItemQueryHelpers queryHelpers)
+ {
+ _dbProvider = dbProvider;
+ _itemTypeLookup = itemTypeLookup;
+ _queryHelpers = queryHelpers;
+ }
+
+ /// <inheritdoc/>
+ public int GetCount(InternalItemsQuery filter)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ _queryHelpers.PrepareFilterQuery(filter);
+
+ using var context = _dbProvider.CreateDbContext();
+ var dbQuery = _queryHelpers.TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
+
+ return dbQuery.Count();
+ }
+
+ /// <inheritdoc />
+ public ItemCounts GetItemCounts(InternalItemsQuery filter)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ _queryHelpers.PrepareFilterQuery(filter);
+
+ using var context = _dbProvider.CreateDbContext();
+ var dbQuery = _queryHelpers.TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
+
+ var counts = dbQuery
+ .GroupBy(x => x.Type)
+ .Select(x => new { x.Key, Count = x.Count() })
+ .ToArray();
+
+ var lookup = _itemTypeLookup.BaseItemKindNames;
+ var result = new ItemCounts
+ {
+ ItemCount = counts.Sum(c => c.Count)
+ };
+ foreach (var count in counts)
+ {
+ if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
+ {
+ result.AlbumCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
+ {
+ result.ArtistCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
+ {
+ result.EpisodeCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
+ {
+ result.MovieCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
+ {
+ result.MusicVideoCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
+ {
+ result.ProgramCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
+ {
+ result.SeriesCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
+ {
+ result.SongCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
+ {
+ result.TrailerCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.BoxSet], StringComparison.Ordinal))
+ {
+ result.BoxSetCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Book], StringComparison.Ordinal))
+ {
+ result.BookCount = count.Count;
+ }
+ }
+
+ return result;
+ }
+
+ /// <inheritdoc />
+ public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, InternalItemsQuery accessFilter)
+ {
+ using var context = _dbProvider.CreateDbContext();
+
+ var item = context.BaseItems.AsNoTracking()
+ .Where(e => e.Id == id)
+ .Select(e => new { e.Name, e.CleanName })
+ .FirstOrDefault();
+
+ if (item is null)
+ {
+ return new ItemCounts();
+ }
+
+ IQueryable<BaseItemEntity> baseQuery;
+ switch (kind)
+ {
+ case BaseItemKind.Person:
+ baseQuery = context.PeopleBaseItemMap
+ .AsNoTracking()
+ .Where(m => m.People.Name == item.Name)
+ .Select(m => m.Item);
+ break;
+ case BaseItemKind.MusicArtist:
+ baseQuery = context.ItemValuesMap
+ .AsNoTracking()
+ .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
+ && (ivm.ItemValue.Type == ItemValueType.Artist || ivm.ItemValue.Type == ItemValueType.AlbumArtist))
+ .Select(ivm => ivm.Item);
+ break;
+ case BaseItemKind.Genre:
+ case BaseItemKind.MusicGenre:
+ baseQuery = context.ItemValuesMap
+ .AsNoTracking()
+ .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
+ && ivm.ItemValue.Type == ItemValueType.Genre)
+ .Select(ivm => ivm.Item);
+ break;
+ case BaseItemKind.Studio:
+ baseQuery = context.ItemValuesMap
+ .AsNoTracking()
+ .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
+ && ivm.ItemValue.Type == ItemValueType.Studios)
+ .Select(ivm => ivm.Item);
+ break;
+ case BaseItemKind.Year:
+ if (int.TryParse(item.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
+ {
+ baseQuery = context.BaseItems
+ .AsNoTracking()
+ .Where(e => e.ProductionYear == year);
+ }
+ else
+ {
+ return new ItemCounts();
+ }
+
+ break;
+ default:
+ return new ItemCounts();
+ }
+
+ var typeNames = relatedItemKinds.Select(k => _itemTypeLookup.BaseItemKindNames[k]).ToArray();
+ baseQuery = baseQuery.Where(e => typeNames.Contains(e.Type));
+
+ baseQuery = _queryHelpers.ApplyAccessFiltering(context, baseQuery, accessFilter);
+
+ var counts = baseQuery
+ .GroupBy(x => x.Type)
+ .Select(x => new { x.Key, Count = x.Count() })
+ .ToArray();
+
+ var lookup = _itemTypeLookup.BaseItemKindNames;
+ var result = new ItemCounts();
+ var totalCount = 0;
+
+ foreach (var count in counts)
+ {
+ totalCount += count.Count;
+
+ if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
+ {
+ result.AlbumCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
+ {
+ result.ArtistCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
+ {
+ result.EpisodeCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
+ {
+ result.MovieCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
+ {
+ result.MusicVideoCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
+ {
+ result.ProgramCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
+ {
+ result.SeriesCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
+ {
+ result.SongCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
+ {
+ result.TrailerCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.BoxSet], StringComparison.Ordinal))
+ {
+ result.BoxSetCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Book], StringComparison.Ordinal))
+ {
+ result.BookCount = count.Count;
+ }
+ }
+
+ result.ItemCount = totalCount;
+
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public int GetPlayedCount(InternalItemsQuery filter, Guid ancestorId)
+ {
+ ArgumentNullException.ThrowIfNull(filter.User);
+ using var dbContext = _dbProvider.CreateDbContext();
+
+ var baseQuery = _queryHelpers.BuildAccessFilteredDescendantsQuery(dbContext, filter, ancestorId);
+ return baseQuery.Count(b => b.UserData!.Any(u => u.UserId == filter.User.Id && u.Played));
+ }
+
+ /// <inheritdoc/>
+ public int GetTotalCount(InternalItemsQuery filter, Guid ancestorId)
+ {
+ using var dbContext = _dbProvider.CreateDbContext();
+
+ var baseQuery = _queryHelpers.BuildAccessFilteredDescendantsQuery(dbContext, filter, ancestorId);
+ return baseQuery.Count();
+ }
+
+ /// <inheritdoc/>
+ public (int Played, int Total) GetPlayedAndTotalCount(InternalItemsQuery filter, Guid ancestorId)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ ArgumentNullException.ThrowIfNull(filter.User);
+ using var dbContext = _dbProvider.CreateDbContext();
+
+ var baseQuery = _queryHelpers.BuildAccessFilteredDescendantsQuery(dbContext, filter, ancestorId);
+ return GetPlayedAndTotalCountFromQuery(baseQuery, filter.User.Id);
+ }
+
+ /// <inheritdoc/>
+ public (int Played, int Total) GetPlayedAndTotalCountFromLinkedChildren(InternalItemsQuery filter, Guid parentId)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ ArgumentNullException.ThrowIfNull(filter.User);
+ using var dbContext = _dbProvider.CreateDbContext();
+
+ var allDescendantIds = DescendantQueryHelper.GetAllDescendantIds(dbContext, parentId);
+ var baseQuery = dbContext.BaseItems
+ .Where(b => allDescendantIds.Contains(b.Id) && !b.IsFolder && !b.IsVirtualItem);
+ baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, filter);
+
+ return GetPlayedAndTotalCountFromQuery(baseQuery, filter.User.Id);
+ }
+
+ /// <inheritdoc/>
+ public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
+ {
+ ArgumentNullException.ThrowIfNull(parentIds);
+
+ if (parentIds.Count == 0)
+ {
+ return new Dictionary<Guid, int>();
+ }
+
+ using var dbContext = _dbProvider.CreateDbContext();
+
+ var parentIdsArray = parentIds.ToArray();
+
+ var hierarchicalCounts = dbContext.BaseItems
+ .Where(b => b.ParentId.HasValue && parentIdsArray.Contains(b.ParentId.Value))
+ .GroupBy(b => b.ParentId!.Value)
+ .Select(g => new { ParentId = g.Key, Count = g.Count() })
+ .ToDictionary(x => x.ParentId, x => x.Count);
+
+ var linkedCounts = dbContext.LinkedChildren
+ .Where(lc => parentIdsArray.Contains(lc.ParentId))
+ .GroupBy(lc => lc.ParentId)
+ .Select(g => new { ParentId = g.Key, Count = g.Count() })
+ .ToDictionary(x => x.ParentId, x => x.Count);
+
+ var result = new Dictionary<Guid, int>();
+ foreach (var parentId in parentIds)
+ {
+ var hierarchicalCount = hierarchicalCounts.GetValueOrDefault(parentId, 0);
+ var linkedCount = linkedCounts.GetValueOrDefault(parentId, 0);
+
+ result[parentId] = linkedCount > 0 ? linkedCount : hierarchicalCount;
+ }
+
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user)
+ {
+ ArgumentNullException.ThrowIfNull(folderIds);
+ ArgumentNullException.ThrowIfNull(user);
+
+ if (folderIds.Count == 0)
+ {
+ return new Dictionary<Guid, (int Played, int Total)>();
+ }
+
+ using var dbContext = _dbProvider.CreateDbContext();
+ var folderIdsArray = folderIds.ToArray();
+ var filter = new InternalItemsQuery(user);
+ var userId = user.Id;
+
+ var leafItems = dbContext.BaseItems
+ .Where(b => !b.IsFolder && !b.IsVirtualItem);
+ leafItems = _queryHelpers.ApplyAccessFiltering(dbContext, leafItems, filter);
+
+ var playedLeafItems = leafItems
+ .Select(b => new { b.Id, Played = b.UserData!.Any(ud => ud.UserId == userId && ud.Played) });
+
+ var ancestorLeaves = dbContext.AncestorIds
+ .WhereOneOrMany(folderIdsArray, a => a.ParentItemId)
+ .Join(
+ playedLeafItems,
+ a => a.ItemId,
+ b => b.Id,
+ (a, b) => new { FolderId = a.ParentItemId, b.Id, b.Played });
+
+ var linkedLeaves = dbContext.LinkedChildren
+ .WhereOneOrMany(folderIdsArray, lc => lc.ParentId)
+ .Join(
+ playedLeafItems,
+ lc => lc.ChildId,
+ b => b.Id,
+ (lc, b) => new { FolderId = lc.ParentId, b.Id, b.Played });
+
+ var linkedFolderLeaves = dbContext.LinkedChildren
+ .WhereOneOrMany(folderIdsArray, lc => lc.ParentId)
+ .Join(
+ dbContext.BaseItems.Where(b => b.IsFolder),
+ lc => lc.ChildId,
+ b => b.Id,
+ (lc, b) => new { lc.ParentId, FolderChildId = b.Id })
+ .Join(
+ dbContext.AncestorIds,
+ x => x.FolderChildId,
+ a => a.ParentItemId,
+ (x, a) => new { x.ParentId, DescendantId = a.ItemId })
+ .Join(
+ playedLeafItems,
+ x => x.DescendantId,
+ b => b.Id,
+ (x, b) => new { FolderId = x.ParentId, b.Id, b.Played });
+
+ var results = ancestorLeaves
+ .Union(linkedLeaves)
+ .Union(linkedFolderLeaves)
+ .GroupBy(x => x.FolderId)
+ .Select(g => new
+ {
+ FolderId = g.Key,
+ Total = g.Select(x => x.Id).Distinct().Count(),
+ Played = g.Where(x => x.Played).Select(x => x.Id).Distinct().Count()
+ })
+ .ToDictionary(x => x.FolderId, x => (x.Played, x.Total));
+
+ return results;
+ }
+
+ private static (int Played, int Total) GetPlayedAndTotalCountFromQuery(IQueryable<BaseItemEntity> query, Guid userId)
+ {
+ var result = query
+ .Select(b => b.UserData!.Any(u => u.UserId == userId && u.Played))
+ .GroupBy(_ => 1)
+ .OrderBy(g => g.Key)
+ .Select(g => new
+ {
+ Total = g.Count(),
+ Played = g.Count(isPlayed => isPlayed)
+ })
+ .FirstOrDefault();
+
+ return result is null ? (0, 0) : (result.Played, result.Total);
+ }
+}