// // Copyright (c) PlaceholderCompany. All rights reserved. // 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; /// /// Repository for obtaining MediaStreams. /// public class MediaStreamRepository : IMediaStreamRepository { private readonly IDbContextFactory _dbProvider; private readonly IServerApplicationHost _serverApplicationHost; private readonly ILocalizationManager _localization; /// /// Initializes a new instance of the class. /// /// The EFCore db factory. /// The Application host. /// The Localisation Provider. public MediaStreamRepository(IDbContextFactory dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) { _dbProvider = dbProvider; _serverApplicationHost = serverApplicationHost; _localization = localization; } /// public async Task SaveMediaStreamsAsync(Guid id, IReadOnlyList 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); } /// public async Task> 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 TranslateQuery(IQueryable 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; } }