Files
pgsql-jellyfin/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
T
wjones 57d6342524 Optimize PostgreSQL dead tuple handling for media scans
Introduce bulk operation extensions and transaction helpers for efficient, batched EF Core operations. Refactor MediaStreamInfo schema by splitting audio/video details into dedicated tables, reducing table bloat and improving update performance. Add migration for data movement and schema changes. Update EF Core models and context for new structure. Enhance PostgreSQL provider configuration and add comprehensive documentation and monitoring guides. Includes minor middleware and code cleanups.
2026-04-30 15:52:37 -04:00

281 lines
10 KiB
C#

// <copyright file="MediaStreamRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Server.Implementations.Item;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using Microsoft.EntityFrameworkCore;
/// <summary>
/// Repository for obtaining MediaStreams.
/// </summary>
public class MediaStreamRepository : IMediaStreamRepository
{
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IServerApplicationHost _serverApplicationHost;
private readonly ILocalizationManager _localization;
/// <summary>
/// Initializes a new instance of the <see cref="MediaStreamRepository"/> class.
/// </summary>
/// <param name="dbProvider">The EFCore db factory.</param>
/// <param name="serverApplicationHost">The Application host.</param>
/// <param name="localization">The Localisation Provider.</param>
public MediaStreamRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization)
{
_dbProvider = dbProvider;
_serverApplicationHost = serverApplicationHost;
_localization = localization;
}
/// <inheritdoc />
public async Task SaveMediaStreamsAsync(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
// Use the execution strategy to handle transactional consistency and retries
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
await context.MediaStreamInfos
.Where(e => e.ItemId.Equals(id))
.ExecuteDeleteAsync(cancellationToken)
.ConfigureAwait(false);
await context.MediaStreamInfos
.AddRangeAsync(streams.Select(f => Map(f, id)), cancellationToken)
.ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IReadOnlyList<MediaStream>> GetMediaStreamsAsync(MediaStreamQuery filter, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
var streams = await TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter)
.Include(e => e.AudioDetails)
.Include(e => e.VideoDetails)
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
return streams.Select(Map).ToArray();
}
private string? GetPathToSave(string? path)
{
if (path is null)
{
return null;
}
return _serverApplicationHost.ReverseVirtualPath(path);
}
private string? RestorePath(string? path)
{
if (path is null)
{
return null;
}
return _serverApplicationHost.ExpandVirtualPath(path);
}
private IQueryable<MediaStreamInfo> TranslateQuery(IQueryable<MediaStreamInfo> query, MediaStreamQuery filter)
{
query = query.Where(e => e.ItemId.Equals(filter.ItemId));
if (filter.Index.HasValue)
{
query = query.Where(e => e.StreamIndex == filter.Index);
}
if (filter.Type.HasValue)
{
var typeValue = (MediaStreamTypeEntity)filter.Type.Value;
query = query.Where(e => e.StreamType == typeValue);
}
return query.OrderBy(e => e.StreamIndex);
}
private MediaStream Map(MediaStreamInfo entity)
{
var dto = new MediaStream();
dto.Index = entity.StreamIndex;
dto.Type = (MediaStreamType)entity.StreamType;
dto.IsAVC = entity.IsAvc;
dto.Codec = entity.Codec;
var language = entity.Language;
// Check if the language has multiple three letter ISO codes
// if yes choose the first as that is the ISO 639-2/T code we're needing
if (language != null && _localization.TryGetISO6392TFromB(language, out string? isoT))
{
language = isoT;
}
dto.Language = language;
dto.Profile = entity.Profile;
dto.Path = RestorePath(entity.Path);
dto.BitRate = entity.BitRate;
dto.IsDefault = entity.IsDefault;
dto.IsForced = entity.IsForced;
dto.IsExternal = entity.IsExternal;
dto.Level = entity.Level;
dto.CodecTag = entity.CodecTag;
dto.Comment = entity.Comment;
dto.Title = entity.Title;
dto.TimeBase = entity.TimeBase;
dto.CodecTimeBase = entity.CodecTimeBase;
if (entity.AudioDetails is { } audio)
{
dto.ChannelLayout = audio.ChannelLayout;
dto.Channels = audio.Channels;
dto.SampleRate = audio.SampleRate;
dto.IsHearingImpaired = audio.IsHearingImpaired.GetValueOrDefault();
}
if (entity.VideoDetails is { } video)
{
dto.AspectRatio = video.AspectRatio;
dto.IsInterlaced = video.IsInterlaced.GetValueOrDefault();
dto.Height = video.Height;
dto.Width = video.Width;
dto.AverageFrameRate = video.AverageFrameRate;
dto.RealFrameRate = video.RealFrameRate;
dto.PixelFormat = video.PixelFormat;
dto.BitDepth = video.BitDepth;
dto.IsAnamorphic = video.IsAnamorphic;
dto.RefFrames = video.RefFrames;
dto.NalLengthSize = video.NalLengthSize;
dto.ColorPrimaries = video.ColorPrimaries;
dto.ColorSpace = video.ColorSpace;
dto.ColorTransfer = video.ColorTransfer;
dto.DvVersionMajor = video.DvVersionMajor;
dto.DvVersionMinor = video.DvVersionMinor;
dto.DvProfile = video.DvProfile;
dto.DvLevel = video.DvLevel;
dto.RpuPresentFlag = video.RpuPresentFlag;
dto.ElPresentFlag = video.ElPresentFlag;
dto.BlPresentFlag = video.BlPresentFlag;
dto.DvBlSignalCompatibilityId = video.DvBlSignalCompatibilityId;
dto.Rotation = video.Rotation;
dto.Hdr10PlusPresentFlag = video.Hdr10PlusPresentFlag;
}
if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
{
dto.LocalizedDefault = _localization.GetLocalizedString("Default");
dto.LocalizedExternal = _localization.GetLocalizedString("External");
if (!string.IsNullOrEmpty(dto.Language))
{
var culture = _localization.FindLanguageInfo(dto.Language);
dto.LocalizedLanguage = culture?.DisplayName;
}
if (dto.Type is MediaStreamType.Subtitle)
{
dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
dto.LocalizedForced = _localization.GetLocalizedString("Forced");
dto.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
}
}
return dto;
}
private MediaStreamInfo Map(MediaStream dto, Guid itemId)
{
var entity = new MediaStreamInfo
{
Item = null!,
ItemId = itemId,
StreamIndex = dto.Index,
StreamType = (MediaStreamTypeEntity)dto.Type,
IsAvc = dto.IsAVC,
Codec = dto.Codec,
Language = dto.Language,
Profile = dto.Profile,
Path = GetPathToSave(dto.Path) ?? dto.Path,
BitRate = dto.BitRate,
IsDefault = dto.IsDefault,
IsForced = dto.IsForced,
IsExternal = dto.IsExternal,
Level = dto.Level.HasValue ? (float)dto.Level : null,
CodecTag = dto.CodecTag,
Comment = dto.Comment,
Title = dto.Title,
TimeBase = dto.TimeBase,
CodecTimeBase = dto.CodecTimeBase,
};
if (dto.Type == MediaStreamType.Audio)
{
entity.AudioDetails = new AudioStreamDetails
{
ItemId = itemId,
StreamIndex = dto.Index,
ChannelLayout = dto.ChannelLayout,
Channels = dto.Channels,
SampleRate = dto.SampleRate,
IsHearingImpaired = dto.IsHearingImpaired,
};
}
else if (dto.Type == MediaStreamType.Video)
{
entity.VideoDetails = new VideoStreamDetails
{
ItemId = itemId,
StreamIndex = dto.Index,
AspectRatio = dto.AspectRatio,
IsInterlaced = dto.IsInterlaced,
Height = dto.Height,
Width = dto.Width,
AverageFrameRate = dto.AverageFrameRate,
RealFrameRate = dto.RealFrameRate,
PixelFormat = dto.PixelFormat,
BitDepth = dto.BitDepth,
IsAnamorphic = dto.IsAnamorphic,
RefFrames = dto.RefFrames,
NalLengthSize = dto.NalLengthSize,
ColorPrimaries = dto.ColorPrimaries,
ColorSpace = dto.ColorSpace,
ColorTransfer = dto.ColorTransfer,
DvVersionMajor = dto.DvVersionMajor,
DvVersionMinor = dto.DvVersionMinor,
DvProfile = dto.DvProfile,
DvLevel = dto.DvLevel,
RpuPresentFlag = dto.RpuPresentFlag,
ElPresentFlag = dto.ElPresentFlag,
BlPresentFlag = dto.BlPresentFlag,
DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId,
Rotation = dto.Rotation,
Hdr10PlusPresentFlag = dto.Hdr10PlusPresentFlag,
};
}
return entity;
}
}