The excessive number of sub-files in the "Home Movies and Photos" media library leads to slow loading or timeout. #8013

Open
opened 2026-02-07 05:33:04 +03:00 by OVERLORD · 1 comment
Owner

Originally created by @lxmou666 on GitHub (Jan 12, 2026).

Description of the bug

If there are too many media files and subdirectories in the "Home Movies and Photos" media library, the front-end page will keep loading. Observations show that the request (/Users/xxx/Items) is extremely slow (taking more than 30 seconds).

Reproduction steps

// Each execution takes more than 1 second

Jellyfin.Server.Implementations.Item.BaseItemRepository
         public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
         ...
Line 272:  
            if (filter.EnableTotalRecordCount)
            {
                 result.TotalRecordCount = dbQuery.Count(); 
            }
         '''

What is the current bug behavior?

Failed to display the list of media files

What is the expected correct behavior?

Fix Jellyfin.Server.Implementations.Item.BaseItemRepository

1、Replace line 378 with

private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
    // This whole block is needed to filter duplicate entries on request
    // for the time being it cannot be used because it would destroy the ordering
    // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but
    // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own

    var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
    if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
    {
        var groupedIds = dbQuery
            .GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey })
            .Select(g => g.Min(e => e.Id));

        dbQuery = context.BaseItems.Where(e => groupedIds.Contains(e.Id));
    }
    else if (enableGroupByPresentationUniqueKey)
    {
        var groupedIds = dbQuery
            .GroupBy(e => e.PresentationUniqueKey)
            .Select(g => g.Min(e => e.Id));

        dbQuery = context.BaseItems.Where(e => groupedIds.Contains(e.Id));
    }
    else if (filter.GroupBySeriesPresentationUniqueKey)
    {
        var groupedIds = dbQuery
            .GroupBy(e => e.SeriesPresentationUniqueKey)
            .Select(g => g.Min(e => e.Id));

        dbQuery = context.BaseItems.Where(e => groupedIds.Contains(e.Id));
    }
    else
    {
        dbQuery = dbQuery.Distinct();
    }

    dbQuery = ApplyOrder(dbQuery, filter, context);

    return dbQuery;
}

2、Replace line 2066 with

        if (filter.IsPlayed.HasValue)
        {
            // We should probably figure this out for all folders, but for right now, this is the only place where we need it
            if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
            {
                baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
            }
            else
            {
                var userId = filter.User!.Id;
                var isPlayed = filter.IsPlayed.Value;

                // Use the default UserData row (CustomDataKey == "") for played status.
                // This avoids per-row correlated subqueries on SQLite.
                baseQuery = baseQuery
                    .GroupJoin(
                        context.UserData.AsNoTracking().Where(u => u.UserId == userId && u.CustomDataKey == string.Empty),
                        item => item.Id,
                        userData => userData.ItemId,
                        (item, userData) => new { Item = item, Played = userData.Select(u => (bool?)u.Played).FirstOrDefault() ?? false })
                    .Where(x => x.Played == isPlayed)
                    .Select(x => x.Item);
            }
        }

3、Replace line 2479 with

        if (filter.AncestorIds.Length > 0)
        {
            // Avoid SQLite translating `Contains(Guid[])` into `json_each(@param)` which is often very slow.
            // An explicit join lets SQLite use the index on `AncestorIds(ParentItemId)`.
            var ancestorIds = filter.AncestorIds;
            baseQuery = baseQuery
                .Join(
                    context.AncestorIds.AsNoTracking().Where(a => ancestorIds.Contains(a.ParentItemId)),
                    item => item.Id,
                    anc => anc.ItemId,
                    (item, _) => item)
                .Distinct();
        }

Jellyfin Server version

10.11.5

Specify commit id

No response

Specify unstable release number

No response

Specify version number

No response

Specify the build version

10.11.5

Environment

- OS: Any
- Linux Kernel:
- Virtualization:
- Clients: Any
- Browser: Any
- FFmpeg Version:
- Playback Method:
- Hardware Acceleration:
- CPU Model:
- GPU Model:
- Plugins:
- Reverse Proxy:
- Base URL:
- Networking:
- Jellyfin Data Storage & Filesystem:
- Media Storage & Filesystem:
- External Integrations:

Jellyfin logs

...

FFmpeg logs


Client / Browser logs

No response

Relevant screenshots or videos

No response

Additional information

No response

Originally created by @lxmou666 on GitHub (Jan 12, 2026). ### Description of the bug If there are too many media files and subdirectories in the "Home Movies and Photos" media library, the front-end page will keep loading. Observations show that the request (/Users/xxx/Items) is extremely slow (taking more than 30 seconds). ### Reproduction steps **// Each execution takes more than 1 second** ``` txt Jellyfin.Server.Implementations.Item.BaseItemRepository public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter) ... Line 272: if (filter.EnableTotalRecordCount) { result.TotalRecordCount = dbQuery.Count(); } ''' ``` ### What is the current _bug_ behavior? Failed to display the list of media files ### What is the expected _correct_ behavior? ## Fix `Jellyfin.Server.Implementations.Item.BaseItemRepository` ### 1、Replace line 378 with ``` csharp private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter) { // This whole block is needed to filter duplicate entries on request // for the time being it cannot be used because it would destroy the ordering // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) { var groupedIds = dbQuery .GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }) .Select(g => g.Min(e => e.Id)); dbQuery = context.BaseItems.Where(e => groupedIds.Contains(e.Id)); } else if (enableGroupByPresentationUniqueKey) { var groupedIds = dbQuery .GroupBy(e => e.PresentationUniqueKey) .Select(g => g.Min(e => e.Id)); dbQuery = context.BaseItems.Where(e => groupedIds.Contains(e.Id)); } else if (filter.GroupBySeriesPresentationUniqueKey) { var groupedIds = dbQuery .GroupBy(e => e.SeriesPresentationUniqueKey) .Select(g => g.Min(e => e.Id)); dbQuery = context.BaseItems.Where(e => groupedIds.Contains(e.Id)); } else { dbQuery = dbQuery.Distinct(); } dbQuery = ApplyOrder(dbQuery, filter, context); return dbQuery; } ``` ### 2、Replace line 2066 with ``` csharp if (filter.IsPlayed.HasValue) { // We should probably figure this out for all folders, but for right now, this is the only place where we need it if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series) { baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)) .Where(e => e.IsFolder == false && e.IsVirtualItem == false) .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played) .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed); } else { var userId = filter.User!.Id; var isPlayed = filter.IsPlayed.Value; // Use the default UserData row (CustomDataKey == "") for played status. // This avoids per-row correlated subqueries on SQLite. baseQuery = baseQuery .GroupJoin( context.UserData.AsNoTracking().Where(u => u.UserId == userId && u.CustomDataKey == string.Empty), item => item.Id, userData => userData.ItemId, (item, userData) => new { Item = item, Played = userData.Select(u => (bool?)u.Played).FirstOrDefault() ?? false }) .Where(x => x.Played == isPlayed) .Select(x => x.Item); } } ``` ### 3、Replace line 2479 with ``` csharp if (filter.AncestorIds.Length > 0) { // Avoid SQLite translating `Contains(Guid[])` into `json_each(@param)` which is often very slow. // An explicit join lets SQLite use the index on `AncestorIds(ParentItemId)`. var ancestorIds = filter.AncestorIds; baseQuery = baseQuery .Join( context.AncestorIds.AsNoTracking().Where(a => ancestorIds.Contains(a.ParentItemId)), item => item.Id, anc => anc.ItemId, (item, _) => item) .Distinct(); } ``` ### Jellyfin Server version 10.11.5 ### Specify commit id _No response_ ### Specify unstable release number _No response_ ### Specify version number _No response_ ### Specify the build version 10.11.5 ### Environment ```markdown - OS: Any - Linux Kernel: - Virtualization: - Clients: Any - Browser: Any - FFmpeg Version: - Playback Method: - Hardware Acceleration: - CPU Model: - GPU Model: - Plugins: - Reverse Proxy: - Base URL: - Networking: - Jellyfin Data Storage & Filesystem: - Media Storage & Filesystem: - External Integrations: ``` ### Jellyfin logs ```shell ... ``` ### FFmpeg logs ```shell ``` ### Client / Browser logs _No response_ ### Relevant screenshots or videos _No response_ ### Additional information _No response_
OVERLORD added the bug label 2026-02-07 05:33:04 +03:00
Author
Owner

@FloFaber commented on GitHub (Jan 28, 2026):

Can confirm this issue on 10.11.6.

@FloFaber commented on GitHub (Jan 28, 2026): Can confirm this issue on 10.11.6.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/jellyfin#8013