aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs13
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs23
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs5
-rw-r--r--Jellyfin.Server/Filters/CachingOpenApiProvider.cs89
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs2
-rw-r--r--MediaBrowser.Controller/IO/FileSystemHelper.cs42
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs28
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs7
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs4
10 files changed, 185 insertions, 30 deletions
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index fad97344b..4d68cb444 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -497,8 +497,17 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual bool AreEqual(string path1, string path2)
{
- return Path.TrimEndingDirectorySeparator(path1).Equals(
- Path.TrimEndingDirectorySeparator(path2),
+ if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2))
+ {
+ return false;
+ }
+
+ var normalized1 = Path.TrimEndingDirectorySeparator(path1);
+ var normalized2 = Path.TrimEndingDirectorySeparator(path2);
+
+ return string.Equals(
+ normalized1,
+ normalized2,
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 2c18ce69a..84168291a 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -618,12 +618,18 @@ public sealed class BaseItemRepository
{
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+ context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
if (entity.Images is { Count: > 0 })
{
context.BaseItemImageInfos.AddRange(entity.Images);
}
+ if (entity.LockedFields is { Count: > 0 })
+ {
+ context.BaseItemMetadataFields.AddRange(entity.LockedFields);
+ }
+
context.BaseItems.Attach(entity).State = EntityState.Modified;
}
}
@@ -1647,19 +1653,18 @@ public sealed class BaseItemRepository
var tags = filter.Tags.ToList();
var excludeTags = filter.ExcludeTags.ToList();
- if (filter.IsMovie == true)
+ if (filter.IsMovie.HasValue)
{
- if (filter.IncludeItemTypes.Length == 0
- || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
- || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
+ var shouldIncludeAllMovieTypes = filter.IsMovie.Value
+ && (filter.IncludeItemTypes.Length == 0
+ || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
+ || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
+
+ if (!shouldIncludeAllMovieTypes)
{
- baseQuery = baseQuery.Where(e => e.IsMovie);
+ baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
}
}
- else if (filter.IsMovie.HasValue)
- {
- baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
- }
if (filter.IsSeries.HasValue)
{
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 08c1a5065..04dd19eda 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -33,9 +33,11 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
@@ -259,7 +261,8 @@ namespace Jellyfin.Server.Extensions
c.OperationFilter<FileRequestFilter>();
c.OperationFilter<ParameterObsoleteFilter>();
c.DocumentFilter<AdditionalModelFilter>();
- });
+ })
+ .Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
}
private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement)
diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
new file mode 100644
index 000000000..4169f2fb3
--- /dev/null
+++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Threading;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.Swagger;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace Jellyfin.Server.Filters;
+
+/// <summary>
+/// OpenApi provider with caching.
+/// </summary>
+internal sealed class CachingOpenApiProvider : ISwaggerProvider
+{
+ private const string CacheKey = "openapi.json";
+
+ private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) };
+ private static readonly SemaphoreSlim _lock = new(1, 1);
+ private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1);
+
+ private readonly IMemoryCache _memoryCache;
+ private readonly SwaggerGenerator _swaggerGenerator;
+ private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class.
+ /// </summary>
+ /// <param name="optionsAccessor">The options accessor.</param>
+ /// <param name="apiDescriptionsProvider">The api descriptions provider.</param>
+ /// <param name="schemaGenerator">The schema generator.</param>
+ /// <param name="memoryCache">The memory cache.</param>
+ public CachingOpenApiProvider(
+ IOptions<SwaggerGeneratorOptions> optionsAccessor,
+ IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
+ ISchemaGenerator schemaGenerator,
+ IMemoryCache memoryCache)
+ {
+ _swaggerGeneratorOptions = optionsAccessor.Value;
+ _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator);
+ _memoryCache = memoryCache;
+ }
+
+ /// <inheritdoc />
+ public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null)
+ {
+ if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null)
+ {
+ return AdjustDocument(openApiDocument, host, basePath);
+ }
+
+ var acquired = _lock.Wait(_lockTimeout);
+ try
+ {
+ if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
+ {
+ return AdjustDocument(openApiDocument, host, basePath);
+ }
+
+ if (!acquired)
+ {
+ throw new InvalidOperationException("OpenApi document is generating");
+ }
+
+ openApiDocument = _swaggerGenerator.GetSwagger(documentName);
+ _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
+ return AdjustDocument(openApiDocument, host, basePath);
+ }
+ finally
+ {
+ if (acquired)
+ {
+ _lock.Release();
+ }
+ }
+ }
+
+ private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath)
+ {
+ document.Servers = _swaggerGeneratorOptions.Servers.Count != 0
+ ? _swaggerGeneratorOptions.Servers
+ : string.IsNullOrEmpty(host) && string.IsNullOrEmpty(basePath)
+ ? []
+ : [new OpenApiServer { Url = $"{host}{basePath}" }];
+
+ return document;
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 151b957fe..59a967725 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1409,7 +1409,7 @@ namespace MediaBrowser.Controller.Entities
if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0))
{
realChildren = realChildren
- .OrderBy(e => e.ProductionYear ?? int.MaxValue)
+ .OrderBy(e => e.PremiereDate ?? DateTime.MaxValue)
.ToArray();
}
diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs
index 3e390ca42..44b7fadf5 100644
--- a/MediaBrowser.Controller/IO/FileSystemHelper.cs
+++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs
@@ -64,6 +64,29 @@ public static class FileSystemHelper
}
/// <summary>
+ /// Resolves a single link hop for the specified path.
+ /// </summary>
+ /// <remarks>
+ /// Returns <c>null</c> if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT).
+ /// </remarks>
+ /// <param name="path">The file path to resolve.</param>
+ /// <returns>
+ /// A <see cref="FileInfo"/> representing the next link target if the path is a link; otherwise, <c>null</c>.
+ /// </returns>
+ private static FileInfo? Resolve(string path)
+ {
+ try
+ {
+ return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo;
+ }
+ catch (IOException)
+ {
+ // Filesystem doesn't support links (e.g., exFAT).
+ return null;
+ }
+ }
+
+ /// <summary>
/// Gets the target of the specified file link.
/// </summary>
/// <remarks>
@@ -84,23 +107,26 @@ public static class FileSystemHelper
if (!returnFinalTarget)
{
- return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo;
- }
-
- if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo)
- {
- return null;
+ return Resolve(linkPath);
}
- if (!targetInfo.Exists)
+ var targetInfo = Resolve(linkPath);
+ if (targetInfo is null || !targetInfo.Exists)
{
return targetInfo;
}
var currentPath = targetInfo.FullName;
var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath };
- while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo)
+
+ while (true)
{
+ var linkInfo = Resolve(currentPath);
+ if (linkInfo is null)
+ {
+ break;
+ }
+
var targetPath = linkInfo.FullName;
// If an infinite loop is detected, return the file info for the
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index a1d891535..843590a1f 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -2378,6 +2378,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
+ // If SDR is the only supported range, we should not copy any of the HDR streams.
+ // All the following copy check assumes at least one HDR format is supported.
+ if (requestedRangeTypes.Length == 1 && requestHasSDR && videoStream.VideoRangeType != VideoRangeType.SDR)
+ {
+ return false;
+ }
+
// If the client does not support DOVI and the video stream is DOVI without fallback, we should not copy it.
if (!requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVI)
{
@@ -5942,28 +5949,37 @@ namespace MediaBrowser.Controller.MediaEncoding
var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap;
var swapOutputWandH = doRkVppTranspose && swapWAndH;
- var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts
+ var outFormat = doOclTonemap ? "p010" : "nv12";
var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
- var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
+ var doScaling = !string.IsNullOrEmpty(GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH));
if (!hasSubs
|| doRkVppTranspose
|| !isFullAfbcPipeline
- || !string.IsNullOrEmpty(doScaling))
+ || doScaling)
{
+ var isScaleRatioSupported = IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f);
+
// RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation,
// but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it
- if (!string.IsNullOrEmpty(doScaling)
- && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
+ if (doScaling && !isScaleRatioSupported)
{
// Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
// Use NV15 instead of P010 to avoid the issue.
// SDR inputs are using BGRA formats already which is not affected.
- var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
+ var intermediateFormat = doOclTonemap ? "nv15" : (isMjpegEncoder ? "bgra" : outFormat);
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1";
mainFilters.Add(hwScaleFilterFirstPass);
}
+ // The RKMPP MJPEG encoder on some newer chip models no longer supports RGB input.
+ // Use 2pass here to enable RGA output of full-range YUV in the 2nd pass.
+ if (isMjpegEncoder && !doOclTonemap && ((doScaling && isScaleRatioSupported) || !doScaling))
+ {
+ var hwScaleFilterFirstPass = "vpp_rkrga=format=bgra:afbc=1";
+ mainFilters.Add(hwScaleFilterFirstPass);
+ }
+
if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose)
{
hwScaleFilter += $":transpose={transposeDir}";
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index 7c0be5a9f..dc20a6d63 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -1,3 +1,4 @@
+using System;
using System.IO;
using System.Linq;
using BDInfo.IO;
@@ -58,6 +59,8 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
}
}
+ private static bool IsHidden(ReadOnlySpan<char> name) => name.StartsWith('.');
+
/// <summary>
/// Gets the directories.
/// </summary>
@@ -65,6 +68,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
public IDirectoryInfo[] GetDirectories()
{
return _fileSystem.GetDirectories(_impl.FullName)
+ .Where(d => !IsHidden(d.Name))
.Select(x => new BdInfoDirectoryInfo(_fileSystem, x))
.ToArray();
}
@@ -76,6 +80,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
public IFileInfo[] GetFiles()
{
return _fileSystem.GetFiles(_impl.FullName)
+ .Where(d => !IsHidden(d.Name))
.Select(x => new BdInfoFileInfo(x))
.ToArray();
}
@@ -88,6 +93,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
public IFileInfo[] GetFiles(string searchPattern)
{
return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
+ .Where(d => !IsHidden(d.Name))
.Select(x => new BdInfoFileInfo(x))
.ToArray();
}
@@ -105,6 +111,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
new[] { searchPattern },
false,
searchOption == SearchOption.AllDirectories)
+ .Where(d => !IsHidden(d.Name))
.Select(x => new BdInfoFileInfo(x))
.ToArray();
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index b7fef842b..73c5b88c8 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -511,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
- if (!isAudio && _proberSupportsFirstVideoFrame)
+ if (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame)
{
args += " -show_frames -only_first_vframe";
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index f220ec4a1..a2102ca9c 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -151,9 +151,9 @@ namespace MediaBrowser.Providers.Manager
.ConfigureAwait(false);
updateType |= beforeSaveResult;
- if (!isFirstRefresh)
+ if (isFirstRefresh)
{
- updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
+ await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
}
// Next run metadata providers