< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Item.ItemCountService
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Item/ItemCountService.cs
Line coverage
1%
Covered lines: 4
Uncovered lines: 233
Coverable lines: 237
Total lines: 431
Line coverage: 1.6%
Branch coverage
0%
Covered branches: 0
Total branches: 75
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/4/2026 - 12:15:16 AM Line coverage: 1.7% (4/235) Branch coverage: 0% (0/75) Total lines: 4278/2/2026 - 12:17:28 AM Line coverage: 1.6% (4/236) Branch coverage: 0% (0/75) Total lines: 4308/3/2026 - 12:16:46 AM Line coverage: 1.6% (4/237) Branch coverage: 0% (0/75) Total lines: 431 5/4/2026 - 12:15:16 AM Line coverage: 1.7% (4/235) Branch coverage: 0% (0/75) Total lines: 4278/2/2026 - 12:17:28 AM Line coverage: 1.6% (4/236) Branch coverage: 0% (0/75) Total lines: 4308/3/2026 - 12:16:46 AM Line coverage: 1.6% (4/237) Branch coverage: 0% (0/75) Total lines: 431

Coverage delta

Coverage delta 1 -1

Metrics

File(s)

/srv/git/jellyfin/Jellyfin.Server.Implementations/Item/ItemCountService.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.Linq;
 7using Jellyfin.Data.Enums;
 8using Jellyfin.Database.Implementations;
 9using Jellyfin.Database.Implementations.Entities;
 10using Jellyfin.Extensions;
 11using MediaBrowser.Controller.Entities;
 12using MediaBrowser.Controller.Persistence;
 13using MediaBrowser.Model.Dto;
 14using Microsoft.EntityFrameworkCore;
 15
 16namespace Jellyfin.Server.Implementations.Item;
 17
 18/// <summary>
 19/// Provides item counting and played-status query operations.
 20/// </summary>
 21public class ItemCountService : IItemCountService
 22{
 23    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 24    private readonly IItemTypeLookup _itemTypeLookup;
 25    private readonly IItemQueryHelpers _queryHelpers;
 26
 27    /// <summary>
 28    /// Initializes a new instance of the <see cref="ItemCountService"/> class.
 29    /// </summary>
 30    /// <param name="dbProvider">The database context factory.</param>
 31    /// <param name="itemTypeLookup">The item type lookup.</param>
 32    /// <param name="queryHelpers">The shared query helpers.</param>
 33    public ItemCountService(
 34        IDbContextFactory<JellyfinDbContext> dbProvider,
 35        IItemTypeLookup itemTypeLookup,
 36        IItemQueryHelpers queryHelpers)
 37    {
 2238        _dbProvider = dbProvider;
 2239        _itemTypeLookup = itemTypeLookup;
 2240        _queryHelpers = queryHelpers;
 2241    }
 42
 43    /// <inheritdoc/>
 44    public int GetCount(InternalItemsQuery filter)
 45    {
 046        ArgumentNullException.ThrowIfNull(filter);
 047        _queryHelpers.PrepareFilterQuery(filter);
 48
 049        using var context = _dbProvider.CreateDbContext();
 050        var dbQuery = _queryHelpers.TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 51
 052        return dbQuery.Count();
 053    }
 54
 55    /// <inheritdoc />
 56    public ItemCounts GetItemCounts(InternalItemsQuery filter)
 57    {
 058        ArgumentNullException.ThrowIfNull(filter);
 059        _queryHelpers.PrepareFilterQuery(filter);
 60
 061        using var context = _dbProvider.CreateDbContext();
 062        var dbQuery = _queryHelpers.TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
 63
 064        var counts = dbQuery
 065            .GroupBy(x => x.Type)
 066            .Select(x => new { x.Key, Count = x.Count() })
 067            .ToArray();
 68
 069        var lookup = _itemTypeLookup.BaseItemKindNames;
 070        var result = new ItemCounts
 071        {
 072            ItemCount = counts.Sum(c => c.Count)
 073        };
 074        foreach (var count in counts)
 75        {
 076            if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
 77            {
 078                result.AlbumCount = count.Count;
 79            }
 080            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
 81            {
 082                result.ArtistCount = count.Count;
 83            }
 084            else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
 85            {
 086                result.EpisodeCount = count.Count;
 87            }
 088            else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
 89            {
 090                result.MovieCount = count.Count;
 91            }
 092            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
 93            {
 094                result.MusicVideoCount = count.Count;
 95            }
 096            else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
 97            {
 098                result.ProgramCount = count.Count;
 99            }
 0100            else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
 101            {
 0102                result.SeriesCount = count.Count;
 103            }
 0104            else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
 105            {
 0106                result.SongCount = count.Count;
 107            }
 0108            else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
 109            {
 0110                result.TrailerCount = count.Count;
 111            }
 0112            else if (string.Equals(count.Key, lookup[BaseItemKind.BoxSet], StringComparison.Ordinal))
 113            {
 0114                result.BoxSetCount = count.Count;
 115            }
 0116            else if (string.Equals(count.Key, lookup[BaseItemKind.Book], StringComparison.Ordinal))
 117            {
 0118                result.BookCount = count.Count;
 119            }
 120        }
 121
 0122        return result;
 0123    }
 124
 125    /// <inheritdoc />
 126    public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, InternalItem
 127    {
 0128        using var context = _dbProvider.CreateDbContext();
 129
 0130        var item = context.BaseItems.AsNoTracking()
 0131            .Where(e => e.Id == id)
 0132            .Select(e => new { e.Name, e.CleanName })
 0133            .FirstOrDefault();
 134
 0135        if (item is null)
 136        {
 0137            return new ItemCounts();
 138        }
 139
 140        IQueryable<BaseItemEntity> baseQuery;
 141        switch (kind)
 142        {
 143            case BaseItemKind.Person:
 0144                baseQuery = ItemsById(context, context.PeopleBaseItemMap
 0145                    .AsNoTracking()
 0146                    .Where(m => m.People.Name == item.Name)
 0147                    .Select(m => m.ItemId));
 0148                break;
 149            case BaseItemKind.MusicArtist:
 0150                baseQuery = ItemsById(context, context.ItemValuesMap
 0151                    .AsNoTracking()
 0152                    .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
 0153                        && (ivm.ItemValue.Type == ItemValueType.Artist || ivm.ItemValue.Type == ItemValueType.AlbumArtis
 0154                    .Select(ivm => ivm.ItemId));
 0155                break;
 156            case BaseItemKind.Genre:
 157            case BaseItemKind.MusicGenre:
 0158                baseQuery = ItemsById(context, context.ItemValuesMap
 0159                    .AsNoTracking()
 0160                    .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
 0161                        && ivm.ItemValue.Type == ItemValueType.Genre)
 0162                    .Select(ivm => ivm.ItemId));
 0163                break;
 164            case BaseItemKind.Studio:
 0165                baseQuery = ItemsById(context, context.ItemValuesMap
 0166                    .AsNoTracking()
 0167                    .Where(ivm => ivm.ItemValue.CleanValue == item.CleanName
 0168                        && ivm.ItemValue.Type == ItemValueType.Studios)
 0169                    .Select(ivm => ivm.ItemId));
 0170                break;
 171            case BaseItemKind.Year:
 0172                if (int.TryParse(item.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
 173                {
 0174                    baseQuery = context.BaseItems
 0175                        .AsNoTracking()
 0176                        .Where(e => e.ProductionYear == year);
 177                }
 178                else
 179                {
 0180                    return new ItemCounts();
 181                }
 182
 183                break;
 184            default:
 0185                return new ItemCounts();
 186        }
 187
 0188        var typeNames = relatedItemKinds.Select(k => _itemTypeLookup.BaseItemKindNames[k]).ToArray();
 0189        baseQuery = baseQuery.Where(e => typeNames.Contains(e.Type));
 190
 0191        baseQuery = _queryHelpers.ApplyAccessFiltering(context, baseQuery, accessFilter);
 192
 0193        var counts = baseQuery
 0194            .GroupBy(x => x.Type)
 0195            .Select(x => new { x.Key, Count = x.Count() })
 0196            .ToArray();
 197
 0198        var lookup = _itemTypeLookup.BaseItemKindNames;
 0199        var result = new ItemCounts();
 0200        var totalCount = 0;
 201
 0202        foreach (var count in counts)
 203        {
 0204            totalCount += count.Count;
 205
 0206            if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
 207            {
 0208                result.AlbumCount = count.Count;
 209            }
 0210            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
 211            {
 0212                result.ArtistCount = count.Count;
 213            }
 0214            else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
 215            {
 0216                result.EpisodeCount = count.Count;
 217            }
 0218            else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
 219            {
 0220                result.MovieCount = count.Count;
 221            }
 0222            else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
 223            {
 0224                result.MusicVideoCount = count.Count;
 225            }
 0226            else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
 227            {
 0228                result.ProgramCount = count.Count;
 229            }
 0230            else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
 231            {
 0232                result.SeriesCount = count.Count;
 233            }
 0234            else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
 235            {
 0236                result.SongCount = count.Count;
 237            }
 0238            else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
 239            {
 0240                result.TrailerCount = count.Count;
 241            }
 0242            else if (string.Equals(count.Key, lookup[BaseItemKind.BoxSet], StringComparison.Ordinal))
 243            {
 0244                result.BoxSetCount = count.Count;
 245            }
 0246            else if (string.Equals(count.Key, lookup[BaseItemKind.Book], StringComparison.Ordinal))
 247            {
 0248                result.BookCount = count.Count;
 249            }
 250        }
 251
 0252        result.ItemCount = totalCount;
 253
 0254        return result;
 0255    }
 256
 257    private static IQueryable<BaseItemEntity> ItemsById(JellyfinDbContext context, IQueryable<Guid> itemIds)
 0258        => context.BaseItems.AsNoTracking().Where(e => itemIds.Contains(e.Id));
 259
 260    /// <inheritdoc/>
 261    public int GetPlayedCount(InternalItemsQuery filter, Guid ancestorId)
 262    {
 0263        ArgumentNullException.ThrowIfNull(filter.User);
 0264        using var dbContext = _dbProvider.CreateDbContext();
 265
 0266        var baseQuery = _queryHelpers.BuildAccessFilteredDescendantsQuery(dbContext, filter, ancestorId);
 0267        return baseQuery.Count(b => b.UserData!.Any(u => u.UserId == filter.User.Id && u.Played));
 0268    }
 269
 270    /// <inheritdoc/>
 271    public int GetTotalCount(InternalItemsQuery filter, Guid ancestorId)
 272    {
 0273        using var dbContext = _dbProvider.CreateDbContext();
 274
 0275        var baseQuery = _queryHelpers.BuildAccessFilteredDescendantsQuery(dbContext, filter, ancestorId);
 0276        return baseQuery.Count();
 0277    }
 278
 279    /// <inheritdoc/>
 280    public (int Played, int Total) GetPlayedAndTotalCount(InternalItemsQuery filter, Guid ancestorId)
 281    {
 0282        ArgumentNullException.ThrowIfNull(filter);
 0283        ArgumentNullException.ThrowIfNull(filter.User);
 0284        using var dbContext = _dbProvider.CreateDbContext();
 285
 0286        var baseQuery = _queryHelpers.BuildAccessFilteredDescendantsQuery(dbContext, filter, ancestorId);
 0287        return GetPlayedAndTotalCountFromQuery(baseQuery, filter.User.Id);
 0288    }
 289
 290    /// <inheritdoc/>
 291    public (int Played, int Total) GetPlayedAndTotalCountFromLinkedChildren(InternalItemsQuery filter, Guid parentId)
 292    {
 0293        ArgumentNullException.ThrowIfNull(filter);
 0294        ArgumentNullException.ThrowIfNull(filter.User);
 0295        using var dbContext = _dbProvider.CreateDbContext();
 296
 0297        var allDescendantIds = DescendantQueryHelper.GetAllDescendantIds(dbContext, parentId);
 0298        var baseQuery = dbContext.BaseItems
 0299            .Where(b => allDescendantIds.Contains(b.Id))
 0300            .Where(DescendantQueryHelper.IsCountableLeaf);
 0301        baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, filter);
 302
 0303        return GetPlayedAndTotalCountFromQuery(baseQuery, filter.User.Id);
 0304    }
 305
 306    /// <inheritdoc/>
 307    public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
 308    {
 0309        ArgumentNullException.ThrowIfNull(parentIds);
 310
 0311        if (parentIds.Count == 0)
 312        {
 0313            return new Dictionary<Guid, int>();
 314        }
 315
 0316        using var dbContext = _dbProvider.CreateDbContext();
 317
 0318        var parentIdsArray = parentIds.ToArray();
 319
 0320        var hierarchicalCounts = dbContext.BaseItems
 0321            .Where(b => b.ParentId.HasValue && parentIdsArray.Contains(b.ParentId.Value))
 0322            .GroupBy(b => b.ParentId!.Value)
 0323            .Select(g => new { ParentId = g.Key, Count = g.Count() })
 0324            .ToDictionary(x => x.ParentId, x => x.Count);
 325
 0326        var linkedCounts = dbContext.LinkedChildren
 0327            .Where(lc => parentIdsArray.Contains(lc.ParentId))
 0328            .GroupBy(lc => lc.ParentId)
 0329            .Select(g => new { ParentId = g.Key, Count = g.Count() })
 0330            .ToDictionary(x => x.ParentId, x => x.Count);
 331
 0332        var result = new Dictionary<Guid, int>();
 0333        foreach (var parentId in parentIds)
 334        {
 0335            var hierarchicalCount = hierarchicalCounts.GetValueOrDefault(parentId, 0);
 0336            var linkedCount = linkedCounts.GetValueOrDefault(parentId, 0);
 337
 0338            result[parentId] = linkedCount > 0 ? linkedCount : hierarchicalCount;
 339        }
 340
 0341        return result;
 0342    }
 343
 344    /// <inheritdoc/>
 345    public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User use
 346    {
 0347        ArgumentNullException.ThrowIfNull(folderIds);
 0348        ArgumentNullException.ThrowIfNull(user);
 349
 0350        if (folderIds.Count == 0)
 351        {
 0352            return new Dictionary<Guid, (int Played, int Total)>();
 353        }
 354
 0355        using var dbContext = _dbProvider.CreateDbContext();
 0356        var folderIdsArray = folderIds.ToArray();
 0357        var filter = new InternalItemsQuery(user);
 0358        var userId = user.Id;
 359
 0360        var leafItems = dbContext.BaseItems
 0361            .Where(DescendantQueryHelper.IsCountableLeaf);
 0362        leafItems = _queryHelpers.ApplyAccessFiltering(dbContext, leafItems, filter);
 363
 0364        var playedLeafItems = leafItems
 0365            .Select(b => new { b.Id, Played = b.UserData!.Any(ud => ud.UserId == userId && ud.Played) });
 366
 0367        var ancestorLeaves = dbContext.AncestorIds
 0368            .WhereOneOrMany(folderIdsArray, a => a.ParentItemId)
 0369            .Join(
 0370                playedLeafItems,
 0371                a => a.ItemId,
 0372                b => b.Id,
 0373                (a, b) => new { FolderId = a.ParentItemId, b.Id, b.Played });
 374
 0375        var linkedLeaves = dbContext.LinkedChildren
 0376            .WhereOneOrMany(folderIdsArray, lc => lc.ParentId)
 0377            .Join(
 0378                playedLeafItems,
 0379                lc => lc.ChildId,
 0380                b => b.Id,
 0381                (lc, b) => new { FolderId = lc.ParentId, b.Id, b.Played });
 382
 0383        var linkedFolderLeaves = dbContext.LinkedChildren
 0384            .WhereOneOrMany(folderIdsArray, lc => lc.ParentId)
 0385            .Join(
 0386                dbContext.BaseItems.Where(b => b.IsFolder),
 0387                lc => lc.ChildId,
 0388                b => b.Id,
 0389                (lc, b) => new { lc.ParentId, FolderChildId = b.Id })
 0390            .Join(
 0391                dbContext.AncestorIds,
 0392                x => x.FolderChildId,
 0393                a => a.ParentItemId,
 0394                (x, a) => new { x.ParentId, DescendantId = a.ItemId })
 0395            .Join(
 0396                playedLeafItems,
 0397                x => x.DescendantId,
 0398                b => b.Id,
 0399                (x, b) => new { FolderId = x.ParentId, b.Id, b.Played });
 400
 0401        var results = ancestorLeaves
 0402            .Union(linkedLeaves)
 0403            .Union(linkedFolderLeaves)
 0404            .GroupBy(x => x.FolderId)
 0405            .Select(g => new
 0406            {
 0407                FolderId = g.Key,
 0408                Total = g.Select(x => x.Id).Distinct().Count(),
 0409                Played = g.Where(x => x.Played).Select(x => x.Id).Distinct().Count()
 0410            })
 0411            .ToDictionary(x => x.FolderId, x => (x.Played, x.Total));
 412
 0413        return results;
 0414    }
 415
 416    private static (int Played, int Total) GetPlayedAndTotalCountFromQuery(IQueryable<BaseItemEntity> query, Guid userId
 417    {
 0418        var result = query
 0419            .Select(b => b.UserData!.Any(u => u.UserId == userId && u.Played))
 0420            .GroupBy(_ => 1)
 0421            .OrderBy(g => g.Key)
 0422            .Select(g => new
 0423            {
 0424                Total = g.Count(),
 0425                Played = g.Count(isPlayed => isPlayed)
 0426            })
 0427            .FirstOrDefault();
 428
 0429        return result is null ? (0, 0) : (result.Played, result.Total);
 430    }
 431}