aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs70
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs186
-rw-r--r--Jellyfin.Server.Implementations/Item/KeyframeRepository.cs64
-rw-r--r--Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs17
-rw-r--r--Jellyfin.Server.Implementations/Item/OrderMapper.cs57
-rw-r--r--Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs109
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs6
7 files changed, 405 insertions, 104 deletions
diff --git a/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs
new file mode 100644
index 000000000..d70ac672f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Jellyfin.Server.Implementations.Extensions;
+
+/// <summary>
+/// Provides <see cref="Expression"/> extension methods.
+/// </summary>
+public static class ExpressionExtensions
+{
+ /// <summary>
+ /// Combines two predicates into a single predicate using a logical OR operation.
+ /// </summary>
+ /// <typeparam name="T">The predicate parameter type.</typeparam>
+ /// <param name="firstPredicate">The first predicate expression to combine.</param>
+ /// <param name="secondPredicate">The second predicate expression to combine.</param>
+ /// <returns>A new expression representing the OR combination of the input predicates.</returns>
+ public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> firstPredicate, Expression<Func<T, bool>> secondPredicate)
+ {
+ ArgumentNullException.ThrowIfNull(firstPredicate);
+ ArgumentNullException.ThrowIfNull(secondPredicate);
+
+ var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters);
+ return Expression.Lambda<Func<T, bool>>(Expression.OrElse(firstPredicate.Body, invokedExpression), firstPredicate.Parameters);
+ }
+
+ /// <summary>
+ /// Combines multiple predicates into a single predicate using a logical OR operation.
+ /// </summary>
+ /// <typeparam name="T">The predicate parameter type.</typeparam>
+ /// <param name="predicates">A collection of predicate expressions to combine.</param>
+ /// <returns>A new expression representing the OR combination of all input predicates.</returns>
+ public static Expression<Func<T, bool>> Or<T>(this IEnumerable<Expression<Func<T, bool>>> predicates)
+ {
+ ArgumentNullException.ThrowIfNull(predicates);
+
+ return predicates.Aggregate((aggregatePredicate, nextPredicate) => aggregatePredicate.Or(nextPredicate));
+ }
+
+ /// <summary>
+ /// Combines two predicates into a single predicate using a logical AND operation.
+ /// </summary>
+ /// <typeparam name="T">The predicate parameter type.</typeparam>
+ /// <param name="firstPredicate">The first predicate expression to combine.</param>
+ /// <param name="secondPredicate">The second predicate expression to combine.</param>
+ /// <returns>A new expression representing the AND combination of the input predicates.</returns>
+ public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> firstPredicate, Expression<Func<T, bool>> secondPredicate)
+ {
+ ArgumentNullException.ThrowIfNull(firstPredicate);
+ ArgumentNullException.ThrowIfNull(secondPredicate);
+
+ var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters);
+ return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(firstPredicate.Body, invokedExpression), firstPredicate.Parameters);
+ }
+
+ /// <summary>
+ /// Combines multiple predicates into a single predicate using a logical AND operation.
+ /// </summary>
+ /// <typeparam name="T">The predicate parameter type.</typeparam>
+ /// <param name="predicates">A collection of predicate expressions to combine.</param>
+ /// <returns>A new expression representing the AND combination of all input predicates.</returns>
+ public static Expression<Func<T, bool>> And<T>(this IEnumerable<Expression<Func<T, bool>>> predicates)
+ {
+ ArgumentNullException.ThrowIfNull(predicates);
+
+ return predicates.Aggregate((aggregatePredicate, nextPredicate) => aggregatePredicate.And(nextPredicate));
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 7b5b6b94d..7f4364cf6 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -7,9 +7,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
@@ -22,6 +20,7 @@ using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using Jellyfin.Server.Implementations.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
@@ -69,7 +68,7 @@ public sealed class BaseItemRepository
private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
- private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Studios];
+ private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
/// <summary>
/// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
@@ -115,6 +114,7 @@ public sealed class BaseItemRepository
context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
+ context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
@@ -535,7 +535,7 @@ public sealed class BaseItemRepository
if (!localItemValueCache.TryGetValue(itemValue, out var refValue))
{
refValue = context.ItemValues
- .Where(f => f.CleanValue == GetCleanValue(itemValue.Value) && (int)f.Type == itemValue.MagicNumber)
+ .Where(f => f.Value == itemValue.Value && (int)f.Type == itemValue.MagicNumber)
.Select(e => e.ItemValueId)
.FirstOrDefault();
}
@@ -620,6 +620,7 @@ public sealed class BaseItemRepository
dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
dto.IsInMixedFolder = entity.IsInMixedFolder;
dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
+ dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
dto.CriticRating = entity.CriticRating;
dto.PresentationUniqueKey = entity.PresentationUniqueKey;
dto.OriginalTitle = entity.OriginalTitle;
@@ -784,6 +785,7 @@ public sealed class BaseItemRepository
entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
entity.IsInMixedFolder = dto.IsInMixedFolder;
entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
+ entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
entity.CriticRating = dto.CriticRating;
entity.PresentationUniqueKey = dto.PresentationUniqueKey;
entity.OriginalTitle = dto.OriginalTitle;
@@ -1209,45 +1211,6 @@ public sealed class BaseItemRepository
return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
}
- private Expression<Func<BaseItemEntity, object>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
- {
-#pragma warning disable CS8603 // Possible null reference return.
- return sortBy switch
- {
- ItemSortBy.AirTime => e => e.SortName, // TODO
- ItemSortBy.Runtime => e => e.RunTimeTicks,
- ItemSortBy.Random => e => EF.Functions.Random(),
- ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.LastPlayedDate,
- ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.PlayCount,
- ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.IsFavorite,
- ItemSortBy.IsFolder => e => e.IsFolder,
- ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
- ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
- ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded,
- ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
- ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
- ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
- ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue,
- // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
- ItemSortBy.SeriesSortName => e => e.SeriesName,
- // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
- ItemSortBy.Album => e => e.Album,
- ItemSortBy.DateCreated => e => e.DateCreated,
- ItemSortBy.PremiereDate => e => e.PremiereDate,
- ItemSortBy.StartDate => e => e.StartDate,
- ItemSortBy.Name => e => e.Name,
- ItemSortBy.CommunityRating => e => e.CommunityRating,
- ItemSortBy.ProductionYear => e => e.ProductionYear,
- ItemSortBy.CriticRating => e => e.CriticRating,
- ItemSortBy.VideoBitRate => e => e.TotalBitrate,
- ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber,
- ItemSortBy.IndexNumber => e => e.IndexNumber,
- _ => e => e.SortName
- };
-#pragma warning restore CS8603 // Possible null reference return.
-
- }
-
private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
{
if (!query.GroupByPresentationUniqueKey)
@@ -1302,7 +1265,7 @@ public sealed class BaseItemRepository
var firstOrdering = orderBy.FirstOrDefault();
if (firstOrdering != default)
{
- var expression = MapOrderByField(firstOrdering.OrderBy, filter);
+ var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
if (firstOrdering.SortOrder == SortOrder.Ascending)
{
orderedQuery = query.OrderBy(expression);
@@ -1327,7 +1290,7 @@ public sealed class BaseItemRepository
foreach (var item in orderBy.Skip(1))
{
- var expression = MapOrderByField(item.OrderBy, filter);
+ var expression = OrderMapper.MapOrderByField(item.OrderBy, filter);
if (item.SortOrder == SortOrder.Ascending)
{
orderedQuery = orderedQuery!.ThenBy(expression);
@@ -1346,34 +1309,39 @@ public sealed class BaseItemRepository
JellyfinDbContext context,
InternalItemsQuery filter)
{
+ const int HDWidth = 1200;
+ const int UHDWidth = 3800;
+ const int UHDHeight = 2100;
+
var minWidth = filter.MinWidth;
var maxWidth = filter.MaxWidth;
var now = DateTime.UtcNow;
- if (filter.IsHD.HasValue)
+ if (filter.IsHD.HasValue || filter.Is4K.HasValue)
{
- const int Threshold = 1200;
- if (filter.IsHD.Value)
- {
- minWidth = Threshold;
- }
- else
+ bool includeSD = false;
+ bool includeHD = false;
+ bool include4K = false;
+
+ if (filter.IsHD.HasValue && !filter.IsHD.Value)
{
- maxWidth = Threshold - 1;
+ includeSD = true;
}
- }
- if (filter.Is4K.HasValue)
- {
- const int Threshold = 3800;
- if (filter.Is4K.Value)
+ if (filter.IsHD.HasValue && filter.IsHD.Value)
{
- minWidth = Threshold;
+ includeHD = true;
}
- else
+
+ if (filter.Is4K.HasValue && filter.Is4K.Value)
{
- maxWidth = Threshold - 1;
+ include4K = true;
}
+
+ baseQuery = baseQuery.Where(e =>
+ (includeSD && e.Width < HDWidth) ||
+ (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
+ (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
}
if (minWidth.HasValue)
@@ -1838,61 +1806,73 @@ public sealed class BaseItemRepository
.Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
}
- if (filter.HasParentalRating ?? false)
+ Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
+ if (filter.MinParentalRating != null)
{
- if (filter.MinParentalRating.HasValue)
+ var min = filter.MinParentalRating;
+ minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue == null;
+ if (min.SubScore != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+ minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScore || e.InheritedParentalRatingValue == null);
}
+ }
- if (filter.MaxParentalRating.HasValue)
+ Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
+ if (filter.MaxParentalRating != null)
+ {
+ var max = filter.MaxParentalRating;
+ maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue == null;
+ if (max.SubScore != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value);
+ maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScore || e.InheritedParentalRatingValue == null);
}
}
- else if (filter.BlockUnratedItems.Length > 0)
+
+ if (filter.HasParentalRating ?? false)
{
- var unratedItems = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
- if (filter.MinParentalRating.HasValue)
+ if (minParentalRatingFilter != null)
{
- if (filter.MaxParentalRating.HasValue)
- {
- baseQuery = baseQuery
- .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
- || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating));
- }
- else
- {
- baseQuery = baseQuery
- .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
- || e.InheritedParentalRatingValue >= filter.MinParentalRating);
- }
+ baseQuery = baseQuery.Where(minParentalRatingFilter);
}
- else
+
+ if (maxParentalRatingFilter != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && !unratedItems.Contains(e.UnratedType));
+ baseQuery = baseQuery.Where(maxParentalRatingFilter);
}
}
- else if (filter.MinParentalRating.HasValue)
+ else if (filter.BlockUnratedItems.Length > 0)
{
- if (filter.MaxParentalRating.HasValue)
+ var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
+ Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType);
+
+ if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value);
+ baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)));
+ }
+ else if (minParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
+ }
+ else if (maxParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
}
else
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+ baseQuery = baseQuery.Where(unratedItemFilter);
}
}
- else if (filter.MaxParentalRating.HasValue)
+ else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value);
+ if (minParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(minParentalRatingFilter);
+ }
+
+ if (maxParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(maxParentalRatingFilter);
+ }
}
else if (!filter.HasParentalRating ?? false)
{
@@ -1987,13 +1967,19 @@ public sealed class BaseItemRepository
if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
{
baseQuery = baseQuery
- .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Artist || f.ItemValue.Type == ItemValueType.AlbumArtist) == 1);
+ .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name));
}
if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
{
baseQuery = baseQuery
- .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Studios) == 1);
+ .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name));
+ }
+
+ if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name));
}
if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
@@ -2116,7 +2102,7 @@ public sealed class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
{
baseQuery = baseQuery
- .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.ParentAncestors!.Any(w => w.ItemId == f.Id)));
+ .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id)));
}
if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
@@ -2151,7 +2137,7 @@ public sealed class BaseItemRepository
{
baseQuery = baseQuery
.Where(e =>
- e.ParentAncestors!
+ e.Parents!
.Any(f =>
f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
@@ -2160,7 +2146,7 @@ public sealed class BaseItemRepository
else
{
baseQuery = baseQuery
- .Where(e => e.ParentAncestors!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
+ .Where(e => e.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
}
}
diff --git a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
new file mode 100644
index 000000000..a2267700f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Controller.Persistence;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/// <summary>
+/// Repository for obtaining Keyframe data.
+/// </summary>
+public class KeyframeRepository : IKeyframeRepository
+{
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="KeyframeRepository"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The EFCore db factory.</param>
+ public KeyframeRepository(IDbContextFactory<JellyfinDbContext> dbProvider)
+ {
+ _dbProvider = dbProvider;
+ }
+
+ private static MediaEncoding.Keyframes.KeyframeData Map(KeyframeData entity)
+ {
+ return new MediaEncoding.Keyframes.KeyframeData(
+ entity.TotalDuration,
+ (entity.KeyframeTicks ?? []).ToList());
+ }
+
+ private KeyframeData Map(MediaEncoding.Keyframes.KeyframeData dto, Guid itemId)
+ {
+ return new()
+ {
+ ItemId = itemId,
+ TotalDuration = dto.TotalDuration,
+ KeyframeTicks = dto.KeyframeTicks.ToList()
+ };
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<MediaEncoding.Keyframes.KeyframeData> GetKeyframeData(Guid itemId)
+ {
+ using var context = _dbProvider.CreateDbContext();
+
+ return context.KeyframeData.AsNoTracking().Where(e => e.ItemId.Equals(itemId)).Select(e => Map(e)).ToList();
+ }
+
+ /// <inheritdoc />
+ public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false);
+ await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
index 36c3b9e56..7eb13b740 100644
--- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
@@ -100,7 +100,18 @@ public class MediaStreamRepository : IMediaStreamRepository
dto.IsAVC = entity.IsAvc;
dto.Codec = entity.Codec;
- dto.Language = entity.Language;
+
+ 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.ChannelLayout = entity.ChannelLayout;
dto.Profile = entity.Profile;
dto.AspectRatio = entity.AspectRatio;
@@ -140,6 +151,7 @@ public class MediaStreamRepository : IMediaStreamRepository
dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId;
dto.IsHearingImpaired = entity.IsHearingImpaired.GetValueOrDefault();
dto.Rotation = entity.Rotation;
+ dto.Hdr10PlusPresentFlag = entity.Hdr10PlusPresentFlag;
if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
{
@@ -207,7 +219,8 @@ public class MediaStreamRepository : IMediaStreamRepository
BlPresentFlag = dto.BlPresentFlag,
DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId,
IsHearingImpaired = dto.IsHearingImpaired,
- Rotation = dto.Rotation
+ Rotation = dto.Rotation,
+ Hdr10PlusPresentFlag = dto.Hdr10PlusPresentFlag,
};
return entity;
}
diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
new file mode 100644
index 000000000..03249b927
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Controller.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/// <summary>
+/// Static class for methods which maps types of ordering to their respecting ordering functions.
+/// </summary>
+public static class OrderMapper
+{
+ /// <summary>
+ /// Creates Func to be executed later with a given BaseItemEntity input for sorting items on query.
+ /// </summary>
+ /// <param name="sortBy">Item property to sort by.</param>
+ /// <param name="query">Context Query.</param>
+ /// <returns>Func to be executed later for sorting query.</returns>
+ public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
+ {
+ return sortBy switch
+ {
+ ItemSortBy.AirTime => e => e.SortName, // TODO
+ ItemSortBy.Runtime => e => e.RunTimeTicks,
+ ItemSortBy.Random => e => EF.Functions.Random(),
+ ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate,
+ ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount,
+ ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite,
+ ItemSortBy.IsFolder => e => e.IsFolder,
+ ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
+ ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
+ ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded,
+ ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
+ ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
+ ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
+ ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue,
+ // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
+ ItemSortBy.SeriesSortName => e => e.SeriesName,
+ // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
+ ItemSortBy.Album => e => e.Album,
+ ItemSortBy.DateCreated => e => e.DateCreated,
+ ItemSortBy.PremiereDate => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
+ ItemSortBy.StartDate => e => e.StartDate,
+ ItemSortBy.Name => e => e.Name,
+ ItemSortBy.CommunityRating => e => e.CommunityRating,
+ ItemSortBy.ProductionYear => e => e.ProductionYear,
+ ItemSortBy.CriticRating => e => e.CriticRating,
+ ItemSortBy.VideoBitRate => e => e.TotalBitrate,
+ ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber,
+ ItemSortBy.IndexNumber => e => e.IndexNumber,
+ _ => e => e.SortName
+ };
+ }
+}
diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
new file mode 100644
index 000000000..635644179
--- /dev/null
+++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.System;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.StorageHelpers;
+
+/// <summary>
+/// Contains methods to help with checking for storage and returning storage data for jellyfin folders.
+/// </summary>
+public static class StorageHelper
+{
+ private const long TwoGigabyte = 2_147_483_647L;
+ private const long FiveHundredAndTwelveMegaByte = 536_870_911L;
+ private static readonly string[] _byteHumanizedSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
+
+ /// <summary>
+ /// Tests the available storage capacity on the jellyfin paths with estimated minimum values.
+ /// </summary>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="logger">Logger.</param>
+ public static void TestCommonPathsForStorageCapacity(IApplicationPaths applicationPaths, ILogger logger)
+ {
+ TestDataDirectorySize(applicationPaths.DataPath, logger, TwoGigabyte);
+ TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte);
+ TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte);
+ TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte);
+ TestDataDirectorySize(applicationPaths.TempDirectory, logger, TwoGigabyte);
+ }
+
+ /// <summary>
+ /// Gets the free space of a specific directory.
+ /// </summary>
+ /// <param name="path">Path to a folder.</param>
+ /// <returns>The number of bytes available space.</returns>
+ public static FolderStorageInfo GetFreeSpaceOf(string path)
+ {
+ try
+ {
+ var driveInfo = new DriveInfo(path);
+ return new FolderStorageInfo()
+ {
+ Path = path,
+ FreeSpace = driveInfo.AvailableFreeSpace,
+ UsedSpace = driveInfo.TotalSize - driveInfo.AvailableFreeSpace,
+ StorageType = driveInfo.DriveType.ToString(),
+ DeviceId = driveInfo.Name,
+ };
+ }
+ catch
+ {
+ return new FolderStorageInfo()
+ {
+ Path = path,
+ FreeSpace = -1,
+ UsedSpace = -1,
+ StorageType = null,
+ DeviceId = null
+ };
+ }
+ }
+
+ /// <summary>
+ /// Gets the underlying drive data from a given path and checks if the available storage capacity matches the threshold.
+ /// </summary>
+ /// <param name="path">The path to a folder to evaluate.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="threshold">The threshold to check for or -1 to just log the data.</param>
+ /// <exception cref="InvalidOperationException">Thrown when the threshold is not available on the underlying storage.</exception>
+ private static void TestDataDirectorySize(string path, ILogger logger, long threshold = -1)
+ {
+ logger.LogDebug("Check path {TestPath} for storage capacity", path);
+ var drive = new DriveInfo(path);
+ if (threshold != -1 && drive.AvailableFreeSpace < threshold)
+ {
+ throw new InvalidOperationException($"The path `{path}` has insufficient free space. Required: at least {HumanizeStorageSize(threshold)}.");
+ }
+
+ logger.LogInformation(
+ "Storage path `{TestPath}` ({StorageType}) successfully checked with {FreeSpace} free which is over the minimum of {MinFree}.",
+ path,
+ drive.DriveType,
+ HumanizeStorageSize(drive.AvailableFreeSpace),
+ HumanizeStorageSize(threshold));
+ }
+
+ /// <summary>
+ /// Formats a size in bytes into a common human readable form.
+ /// </summary>
+ /// <remarks>
+ /// Taken and slightly modified from https://stackoverflow.com/a/4975942/1786007 .
+ /// </remarks>
+ /// <param name="byteCount">The size in bytes.</param>
+ /// <returns>A human readable approximate representation of the argument.</returns>
+ public static string HumanizeStorageSize(long byteCount)
+ {
+ if (byteCount == 0)
+ {
+ return $"0{_byteHumanizedSuffixes[0]}";
+ }
+
+ var bytes = Math.Abs(byteCount);
+ var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
+ var num = Math.Round(bytes / Math.Pow(1024, place), 1);
+ return (Math.Sign(byteCount) * num).ToString(CultureInfo.InvariantCulture) + _byteHumanizedSuffixes[place];
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 3c39e5503..3dfb14d71 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -342,7 +342,8 @@ namespace Jellyfin.Server.Implementations.Users
},
Policy = new UserPolicy
{
- MaxParentalRating = user.MaxParentalAgeRating,
+ MaxParentalRating = user.MaxParentalRatingScore,
+ MaxParentalSubRating = user.MaxParentalRatingSubScore,
EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
AuthenticationProviderId = user.AuthenticationProviderId,
@@ -668,7 +669,8 @@ namespace Jellyfin.Server.Implementations.Users
_ => policy.LoginAttemptsBeforeLockout
};
- user.MaxParentalAgeRating = policy.MaxParentalRating;
+ user.MaxParentalRatingScore = policy.MaxParentalRating;
+ user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
user.AuthenticationProviderId = policy.AuthenticationProviderId;