//
// 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;
}
}