Add SQL query patterns documentation and Linux package build scripts

- Created `TV_SHOWS_SQL_QUERY_PATTERNS.md` to document SQL query patterns for TV shows, including performance issues and missing indexes.
- Added `README.md` for Linux package building, detailing steps for creating Debian and Red Hat packages.
- Implemented build scripts for Debian and Red Hat, including service files and post-installation hooks.
- Added necessary scripts for managing Jellyfin service lifecycle on both Debian and Red Hat systems.
- Included package specifications and installation instructions for both distributions.
This commit is contained in:
2026-07-14 10:32:56 -04:00
parent e51d3577ce
commit 8914f4dce9
29 changed files with 4123 additions and 31 deletions
@@ -450,14 +450,31 @@ public sealed class BaseItemRepository
// Apply ordering before grouping so we get the right items
dbQuery = ApplyOrder(dbQuery, filter, context);
// Get IDs only, without DistinctBy to avoid translation errors
var allIds = await dbQuery
.Select(e => new { e.Id, e.PresentationUniqueKey, SeriesPresentationUniqueKey = e.TvExtras!.SeriesPresentationUniqueKey })
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
// Optimize: Use database-level deduplication to avoid loading all items into memory
var enableGrouping = EnableGroupByPresentationUniqueKey(filter);
List<Guid> filteredIds;
if (enableGrouping && filter.GroupBySeriesPresentationUniqueKey)
{
// Use database-level grouping for TV Shows to avoid loading all episodes into memory
filteredIds = await ApplySeriesGroupingAtDatabaseLevel(dbQuery, context, cancellationToken).ConfigureAwait(false);
}
else if (enableGrouping)
{
// Use database-level grouping for movies, videos, etc. (by PresentationUniqueKey) to avoid loading duplicates into memory
filteredIds = await ApplyPresentationUniqueKeyGrouping(dbQuery, context, cancellationToken).ConfigureAwait(false);
}
else
{
// Get IDs only, without DistinctBy to avoid translation errors
var allIds = await dbQuery
.Select(e => new { e.Id, e.PresentationUniqueKey, SeriesPresentationUniqueKey = e.TvExtras!.SeriesPresentationUniqueKey })
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
// Apply grouping/distinct in memory
var filteredIds = ApplyGroupingInMemory(allIds, filter);
// Apply grouping/distinct in memory
filteredIds = ApplyGroupingInMemory(allIds, filter);
}
// Apply paging to IDs
var pagedIds = ApplyPagingToIds(filteredIds, filter);
@@ -502,24 +519,41 @@ public sealed class BaseItemRepository
// Apply ordering first, before grouping
dbQuery = ApplyOrder(dbQuery, filter, context);
// Get IDs and keys to memory WITHOUT DistinctBy (which can't be translated)
var itemsWithKeys = await dbQuery
.Select(e => new
{
e.Id,
e.PresentationUniqueKey,
SeriesPresentationUniqueKey = e.TvExtras!.SeriesPresentationUniqueKey
})
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
// Optimize: Use database-level deduplication to avoid loading all items into memory
var enableGrouping = EnableGroupByPresentationUniqueKey(filter);
List<Guid> groupedIds;
if (itemsWithKeys.Count == 0)
if (enableGrouping && filter.GroupBySeriesPresentationUniqueKey)
{
return Array.Empty<BaseItemDto>();
// Use database-level grouping for TV Shows to avoid loading all episodes into memory
groupedIds = await ApplySeriesGroupingAtDatabaseLevel(dbQuery, context, cancellationToken).ConfigureAwait(false);
}
else if (enableGrouping)
{
// Use database-level grouping for movies, videos, etc. (by PresentationUniqueKey) to avoid loading duplicates into memory
groupedIds = await ApplyPresentationUniqueKeyGrouping(dbQuery, context, cancellationToken).ConfigureAwait(false);
}
else
{
// Get IDs and keys to memory WITHOUT DistinctBy (which can't be translated)
var itemsWithKeys = await dbQuery
.Select(e => new
{
e.Id,
e.PresentationUniqueKey,
SeriesPresentationUniqueKey = e.TvExtras!.SeriesPresentationUniqueKey
})
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
// Apply grouping in memory
var groupedIds = ApplyGroupingInMemory(itemsWithKeys, filter);
if (itemsWithKeys.Count == 0)
{
return Array.Empty<BaseItemDto>();
}
// Apply grouping in memory
groupedIds = ApplyGroupingInMemory(itemsWithKeys, filter);
}
// Apply paging to IDs
var pagedIds = ApplyPagingToIds(groupedIds, filter);
@@ -852,6 +886,42 @@ public sealed class BaseItemRepository
return filtered.Select(e => (Guid)idProp.GetValue(e)!).ToList();
}
/// <summary>
/// Applies series grouping at the database level using GROUP BY to avoid loading all episodes into memory.
/// This is crucial for TV Shows performance - instead of loading 10,000 episodes and deduplicating in-memory,
/// we group by SeriesPresentationUniqueKey at the database level and only load one episode per series.
/// </summary>
private async Task<List<Guid>> ApplySeriesGroupingAtDatabaseLevel(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, CancellationToken cancellationToken)
{
// Get distinct series presentation keys along with one ID per series
// This uses GROUP BY at the database level instead of in-memory DISTINCT
var groupedBySeriesIds = await dbQuery
.GroupBy(e => e.TvExtras!.SeriesPresentationUniqueKey)
.Select(g => g.First().Id)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return groupedBySeriesIds;
}
/// <summary>
/// Applies presentation unique key grouping at the database level using GROUP BY to avoid loading duplicate items into memory.
/// This optimization applies to movies, music videos, and other media types that can have duplicate PresentationUniqueKey values.
/// Instead of loading all items and deduplicating in-memory, we group by PresentationUniqueKey at the database level.
/// </summary>
private async Task<List<Guid>> ApplyPresentationUniqueKeyGrouping(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, CancellationToken cancellationToken)
{
// Get distinct presentation keys along with one ID per unique key
// This uses GROUP BY at the database level instead of in-memory DISTINCT
var groupedByPresentationKeyIds = await dbQuery
.GroupBy(e => e.PresentationUniqueKey)
.Select(g => g.First().Id)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return groupedByPresentationKeyIds;
}
private List<Guid> ApplyPagingToIds(List<Guid> ids, InternalItemsQuery filter)
{
IEnumerable<Guid> paged = ids;