diff options
Diffstat (limited to 'MediaBrowser.Controller')
14 files changed, 451 insertions, 88 deletions
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index c2efa4ad3..bb0b26b8e 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1423,23 +1423,14 @@ namespace MediaBrowser.Controller.Entities public virtual bool RequiresRefresh() { - if (string.IsNullOrEmpty(Path) || DateModified == default) + if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue) { return false; } var info = FileSystem.GetFileSystemInfo(Path); - if (info.Exists) - { - if (info.IsDirectory) - { - return info.LastWriteTimeUtc != DateModified; - } - return info.LastWriteTimeUtc != DateModified; - } - - return false; + return info.Exists && this.HasChanged(info.LastWriteTimeUtc); } public virtual List<string> GetUserDataKeys() @@ -2002,9 +1993,10 @@ namespace MediaBrowser.Controller.Entities } // Remove from file system - if (info.IsLocalFile) + var path = info.Path; + if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path)) { - FileSystem.DeleteFile(info.Path); + FileSystem.DeleteFile(path); } // Remove from item diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index dcd22a3b4..668e2c1e2 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -114,5 +114,19 @@ namespace MediaBrowser.Controller.Entities source.DeepCopy(dest); return dest; } + + /// <summary> + /// Determines if the item has changed. + /// </summary> + /// <param name="source">The source object.</param> + /// <param name="asOf">The timestamp to detect changes as of.</param> + /// <typeparam name="T">Source type.</typeparam> + /// <returns>Whether the item has changed.</returns> + public static bool HasChanged<T>(this T source, DateTime asOf) + where T : BaseItem + { + ArgumentNullException.ThrowIfNull(source); + return source.DateModified.Subtract(asOf).Duration().TotalSeconds > 1; + } } } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index e0c3b0a93..06cbcc2e1 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -11,7 +11,6 @@ using System.Security; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; using J2N.Collections.Generic.Extensions; using Jellyfin.Data; using Jellyfin.Data.Enums; @@ -25,6 +24,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LibraryTaskScheduler; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; @@ -49,6 +49,8 @@ namespace MediaBrowser.Controller.Entities public static IUserViewManager UserViewManager { get; set; } + public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance is root. /// </summary> @@ -598,51 +600,13 @@ namespace MediaBrowser.Controller.Entities /// <returns>Task.</returns> private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progress, CancellationToken cancellationToken) { - var childrenCount = children.Count; - var childrenProgress = new double[childrenCount]; - - void UpdateProgress() - { - progress.Report(childrenProgress.Average()); - } - - var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; - var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount; - - var actionBlock = new ActionBlock<int>( - async i => - { - var innerProgress = new Progress<double>(innerPercent => - { - // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls - var innerPercentRounded = Math.Round(innerPercent); - if (childrenProgress[i] != innerPercentRounded) - { - childrenProgress[i] = innerPercentRounded; - UpdateProgress(); - } - }); - - await task(children[i], innerProgress).ConfigureAwait(false); - - childrenProgress[i] = 100; - - UpdateProgress(); - }, - new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = parallelism, - CancellationToken = cancellationToken, - }); - - for (var i = 0; i < childrenCount; i++) - { - await actionBlock.SendAsync(i, cancellationToken).ConfigureAwait(false); - } - - actionBlock.Complete(); - - await actionBlock.Completion.ConfigureAwait(false); + await LimitedConcurrencyLibraryScheduler + .Enqueue( + children.ToArray(), + task, + progress, + cancellationToken) + .ConfigureAwait(false); } /// <summary> @@ -731,7 +695,7 @@ namespace MediaBrowser.Controller.Entities items = GetRecursiveChildren(user, query); } - return PostFilterAndSort(items, query, true); + return PostFilterAndSort(items, query); } if (this is not UserRootFolder @@ -995,10 +959,10 @@ namespace MediaBrowser.Controller.Entities items = GetChildren(user, true, childQuery).Where(filter); } - return PostFilterAndSort(items, query, true); + return PostFilterAndSort(items, query); } - protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting) + protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query) { var user = query.User; @@ -1008,7 +972,7 @@ namespace MediaBrowser.Controller.Entities items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager); } - #pragma warning disable CA1309 +#pragma warning disable CA1309 if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater)) { items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1); @@ -1023,7 +987,7 @@ namespace MediaBrowser.Controller.Entities { items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1); } - #pragma warning restore CA1309 +#pragma warning restore CA1309 // This must be the last filter if (!query.AdjacentTo.IsNullOrEmpty()) @@ -1031,7 +995,7 @@ namespace MediaBrowser.Controller.Entities items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } - return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting); + return UserViewBuilder.SortAndPage(items, null, query, LibraryManager); } private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded( diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index d50f3d075..b32b64f5d 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Diacritics.Extensions; using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; @@ -373,8 +374,15 @@ namespace MediaBrowser.Controller.Entities .Where(i => i != other) .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray(); - ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); - IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags); + ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags) + .Where(tag => !string.IsNullOrWhiteSpace(tag)) + .Select(tag => tag.RemoveDiacritics().ToLowerInvariant()) + .ToArray(); + + IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags) + .Where(tag => !string.IsNullOrWhiteSpace(tag)) + .Select(tag => tag.RemoveDiacritics().ToLowerInvariant()) + .ToArray(); User = user; } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 408161b03..48211d99f 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -179,7 +179,7 @@ namespace MediaBrowser.Controller.Entities.TV var items = GetEpisodes(user, query.DtoOptions, true).Where(filter); - return PostFilterAndSort(items, query, false); + return PostFilterAndSort(items, query); } /// <summary> diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index b4ad05921..62c73d56f 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; query.IncludeItemTypes = new[] { BaseItemKind.Season }; - query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; + query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) }; if (user is not null && !user.DisplayMissingEpisodes) { @@ -247,10 +247,6 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; - if (query.OrderBy.Count == 0) - { - query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; - } if (query.IncludeItemTypes.Length == 0) { diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index bc7e22d9a..deed3631b 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.Entities PresetViews = query.PresetViews }); - return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true); + return UserViewBuilder.SortAndPage(result, null, query, LibraryManager); } public override int GetChildCount(User user) diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 1eb3c8f50..7679d383f 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -438,22 +438,18 @@ namespace MediaBrowser.Controller.Entities items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } - return SortAndPage(items, totalRecordLimit, query, libraryManager, true); + return SortAndPage(items, totalRecordLimit, query, libraryManager); } public static QueryResult<BaseItem> SortAndPage( IEnumerable<BaseItem> items, int? totalRecordLimit, InternalItemsQuery query, - ILibraryManager libraryManager, - bool enableSorting) + ILibraryManager libraryManager) { - if (enableSorting) + if (query.OrderBy.Count > 0) { - if (query.OrderBy.Count > 0) - { - items = libraryManager.Sort(items, query.User, query.OrderBy); - } + items = libraryManager.Sort(items, query.User, query.OrderBy); } var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray(); diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 0109cf4b7..7f06a318a 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -34,6 +34,12 @@ namespace MediaBrowser.Controller.Library IEnumerable<Guid> UsersIds { get; } /// <summary> + /// Checks if the user's username is valid. + /// </summary> + /// <param name="name">The user's username.</param> + void ThrowIfInvalidUsername(string name); + + /// <summary> /// Initializes the user manager and ensures that a user exists. /// </summary> /// <returns>Awaitable task.</returns> diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/ILimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/ILimitedConcurrencyLibraryScheduler.cs new file mode 100644 index 000000000..e7460a2e6 --- /dev/null +++ b/MediaBrowser.Controller/LibraryTaskScheduler/ILimitedConcurrencyLibraryScheduler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Controller.LibraryTaskScheduler; + +/// <summary> +/// Provides a shared scheduler to run library related tasks based on the <see cref="ServerConfiguration.LibraryScanFanoutConcurrency"/>. +/// </summary> +public interface ILimitedConcurrencyLibraryScheduler +{ + /// <summary> + /// Enqueues an action that will be invoked with the set data. + /// </summary> + /// <typeparam name="T">The data Type.</typeparam> + /// <param name="data">The data.</param> + /// <param name="worker">The callback to process the data.</param> + /// <param name="progress">A progress reporter.</param> + /// <param name="cancellationToken">Stop token.</param> + /// <returns>A task that finishes when all data has been processed by the worker.</returns> + Task Enqueue<T>(T[] data, Func<T, IProgress<double>, Task> worker, IProgress<double> progress, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs new file mode 100644 index 000000000..0de5f198d --- /dev/null +++ b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.LibraryTaskScheduler; + +/// <summary> +/// Provides Parallel action interface to process tasks with a set concurrency level. +/// </summary> +public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibraryScheduler, IAsyncDisposable +{ + private const int CleanupGracePeriod = 60; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly ILogger<LimitedConcurrencyLibraryScheduler> _logger; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly Dictionary<CancellationTokenSource, Task> _taskRunners = new(); + + private static readonly AsyncLocal<CancellationTokenSource> _deadlockDetector = new(); + + /// <summary> + /// Gets used to lock all operations on the Tasks queue and creating workers. + /// </summary> + private readonly Lock _taskLock = new(); + + private readonly BlockingCollection<TaskQueueItem> _tasks = new(); + + private volatile int _workCounter; + private Task? _cleanupTask; + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="LimitedConcurrencyLibraryScheduler"/> class. + /// </summary> + /// <param name="hostApplicationLifetime">The hosting lifetime.</param> + /// <param name="logger">The logger.</param> + /// <param name="serverConfigurationManager">The server configuration manager.</param> + public LimitedConcurrencyLibraryScheduler( + IHostApplicationLifetime hostApplicationLifetime, + ILogger<LimitedConcurrencyLibraryScheduler> logger, + IServerConfigurationManager serverConfigurationManager) + { + _hostApplicationLifetime = hostApplicationLifetime; + _logger = logger; + _serverConfigurationManager = serverConfigurationManager; + } + + private void ScheduleTaskCleanup() + { + lock (_taskLock) + { + if (_cleanupTask is not null) + { + _logger.LogDebug("Cleanup task already scheduled."); + // cleanup task is already running. + return; + } + + _cleanupTask = RunCleanupTask(); + } + + async Task RunCleanupTask() + { + _logger.LogDebug("Schedule cleanup task in {CleanupGracePerioid} sec.", CleanupGracePeriod); + await Task.Delay(TimeSpan.FromSeconds(CleanupGracePeriod)).ConfigureAwait(false); + if (_disposed) + { + _logger.LogDebug("Abort cleaning up, already disposed."); + return; + } + + lock (_taskLock) + { + if (_tasks.Count > 0 || _workCounter > 0) + { + _logger.LogDebug("Delay cleanup task, operations still running."); + // tasks are still there so its still in use. Reschedule cleanup task. + // we cannot just exit here and rely on the other invoker because there is a considerable timeframe where it could have already ended. + _cleanupTask = RunCleanupTask(); + return; + } + } + + _logger.LogDebug("Cleanup runners."); + foreach (var item in _taskRunners.ToArray()) + { + await item.Key.CancelAsync().ConfigureAwait(false); + _taskRunners.Remove(item.Key); + } + } + } + + private bool ShouldForceSequentialOperation() + { + // if the user either set the setting to 1 or it's unset and we have fewer than 4 cores it's better to run sequentially. + var fanoutSetting = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency; + return fanoutSetting == 1 || (fanoutSetting <= 0 && Environment.ProcessorCount <= 3); + } + + private int CalculateScanConcurrencyLimit() + { + // when this is invoked, we already checked ShouldForceSequentialOperation for the sequential check. + var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency; + if (fanoutConcurrency <= 0) + { + // in case the user did not set a limit manually, we can assume he has 3 or more cores as already checked by ShouldForceSequentialOperation. + return Environment.ProcessorCount - 3; + } + + return fanoutConcurrency; + } + + private void Worker() + { + lock (_taskLock) + { + var operationFanout = Math.Max(0, CalculateScanConcurrencyLimit() - _taskRunners.Count); + _logger.LogDebug("Spawn {NumberRunners} new runners.", operationFanout); + for (int i = 0; i < operationFanout; i++) + { + var stopToken = new CancellationTokenSource(); + var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, _hostApplicationLifetime.ApplicationStopping); + _taskRunners.Add( + combinedSource, + Task.Factory.StartNew( + ItemWorker, + (combinedSource, stopToken), + combinedSource.Token, + TaskCreationOptions.PreferFairness, + TaskScheduler.Default)); + } + } + } + + private async Task ItemWorker(object? obj) + { + var stopToken = ((CancellationTokenSource TaskStop, CancellationTokenSource GlobalStop))obj!; + _deadlockDetector.Value = stopToken.TaskStop; + try + { + foreach (var item in _tasks.GetConsumingEnumerable(stopToken.GlobalStop.Token)) + { + stopToken.GlobalStop.Token.ThrowIfCancellationRequested(); + try + { + var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0; + Debug.Assert(newWorkerLimit, "_workCounter > 0"); + _logger.LogDebug("Process new item '{Data}'.", item.Data); + await ProcessItem(item).ConfigureAwait(false); + } + finally + { + var newWorkerLimit = Interlocked.Decrement(ref _workCounter) >= 0; + Debug.Assert(newWorkerLimit, "_workCounter > 0"); + } + } + } + catch (OperationCanceledException) when (stopToken.TaskStop.IsCancellationRequested) + { + // thats how you do it, interupt the waiter thread. There is nothing to do here when it was on purpose. + } + finally + { + _logger.LogDebug("Cleanup Runner'."); + _deadlockDetector.Value = default!; + _taskRunners.Remove(stopToken.TaskStop); + stopToken.GlobalStop.Dispose(); + stopToken.TaskStop.Dispose(); + } + } + + private async Task ProcessItem(TaskQueueItem item) + { + try + { + if (item.CancellationToken.IsCancellationRequested) + { + // if item is cancelled, just skip it + return; + } + + await item.Worker(item.Data).ConfigureAwait(true); + } + catch (System.Exception ex) + { + _logger.LogError(ex, "Error while performing a library operation"); + } + finally + { + item.Progress.Report(100); + item.Done.SetResult(); + } + } + + /// <inheritdoc/> + public async Task Enqueue<T>(T[] data, Func<T, IProgress<double>, Task> worker, IProgress<double> progress, CancellationToken cancellationToken) + { + if (_disposed) + { + return; + } + + if (data.Length == 0 || cancellationToken.IsCancellationRequested) + { + progress.Report(100); + return; + } + + _logger.LogDebug("Enqueue new Workset of {NoItems} items.", data.Length); + + TaskQueueItem[] workItems = null!; + + void UpdateProgress() + { + progress.Report(workItems.Select(e => e.ProgressValue).Average()); + } + + workItems = data.Select(item => + { + TaskQueueItem queueItem = null!; + return queueItem = new TaskQueueItem() + { + Data = item!, + Progress = new Progress<double>(innerPercent => + { + // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls + var innerPercentRounded = Math.Round(innerPercent); + if (queueItem.ProgressValue != innerPercentRounded) + { + queueItem.ProgressValue = innerPercentRounded; + UpdateProgress(); + } + }), + Worker = (val) => worker((T)val, queueItem.Progress), + CancellationToken = cancellationToken + }; + }).ToArray(); + + if (ShouldForceSequentialOperation()) + { + _logger.LogDebug("Process sequentially."); + try + { + foreach (var item in workItems) + { + await ProcessItem(item).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // operation is cancelled. Do nothing. + } + + _logger.LogDebug("Process sequentially done."); + return; + } + + for (var i = 0; i < workItems.Length; i++) + { + var item = workItems[i]!; + _tasks.Add(item, CancellationToken.None); + } + + if (_deadlockDetector.Value is not null) + { + _logger.LogDebug("Nested invocation detected, process in-place."); + try + { + // we are in a nested loop. There is no reason to spawn a task here as that would just lead to deadlocks and no additional concurrency is achieved + while (workItems.Any(e => !e.Done.Task.IsCompleted) && _tasks.TryTake(out var item, 200, _deadlockDetector.Value.Token)) + { + await ProcessItem(item).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (_deadlockDetector.Value.IsCancellationRequested) + { + // operation is cancelled. Do nothing. + } + + _logger.LogDebug("process in-place done."); + } + else + { + Worker(); + _logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length); + await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false); + _logger.LogDebug("{NoWorkers} completed.", workItems.Length); + ScheduleTaskCleanup(); + } + } + + /// <inheritdoc/> + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + _tasks.CompleteAdding(); + foreach (var item in _taskRunners) + { + await item.Key.CancelAsync().ConfigureAwait(false); + } + + _tasks.Dispose(); + if (_cleanupTask is not null) + { + await _cleanupTask.ConfigureAwait(false); + _cleanupTask?.Dispose(); + } + } + + private class TaskQueueItem + { + public required object Data { get; init; } + + public double ProgressValue { get; set; } + + public required Func<object, Task> Worker { get; init; } + + public required IProgress<double> Progress { get; init; } + + public TaskCompletionSource Done { get; } = new(); + + public CancellationToken CancellationToken { get; init; } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 364470cd2..8d3977103 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -230,10 +230,10 @@ namespace MediaBrowser.Controller.MediaEncoding { var hwType = encodingOptions.HardwareAccelerationType; - // Only Intel has VA-API MJPEG encoder + // Only enable VA-API MJPEG encoder on Intel iHD driver. + // Legacy platforms supported ONLY by i965 do not support MJPEG encoder. if (hwType == HardwareAccelerationType.vaapi - && !(_mediaEncoder.IsVaapiDeviceInteliHD - || _mediaEncoder.IsVaapiDeviceInteli965)) + && !_mediaEncoder.IsVaapiDeviceInteliHD) { return _defaultMjpegEncoder; } @@ -2376,6 +2376,13 @@ namespace MediaBrowser.Controller.MediaEncoding var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase); + var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); + + // 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) + { + return false; + } if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase) && !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10) @@ -2383,6 +2390,12 @@ namespace MediaBrowser.Controller.MediaEncoding || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR) || (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus))) { + // If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG. + if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG) + { + return false; + } + // Check complicated cases where we need to remove dynamic metadata // Conservatively refuse to copy if the encoder can't remove dynamic metadata, // but a removal is required for compatability reasons. @@ -4435,6 +4448,13 @@ namespace MediaBrowser.Controller.MediaEncoding var swapOutputWandH = doVppTranspose && swapWAndH; var hwScaleFilter = GetHwScaleFilter("vpp", "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + // d3d11va doesn't support dynamic pool size, use vpp filter ctx to relay + // to prevent encoder async and bframes from exhausting the decoder pool. + if (!string.IsNullOrEmpty(hwScaleFilter) && isD3d11vaDecoder) + { + hwScaleFilter += ":passthrough=0"; + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose) { hwScaleFilter += $":transpose={transposeDir}"; @@ -7131,7 +7151,8 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier += " -async " + state.InputAudioSync; } - if (!string.IsNullOrEmpty(state.InputVideoSync)) + // The -fps_mode option cannot be applied to input + if (!string.IsNullOrEmpty(state.InputVideoSync) && _mediaEncoder.EncoderVersion < new Version(5, 1)) { inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion); } diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs index 720c607f1..4f13a7ecc 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs @@ -20,10 +20,10 @@ public interface IMediaSegmentManager /// </summary> /// <param name="baseItem">The Item to evaluate.</param> /// <param name="libraryOptions">The library options.</param> - /// <param name="overwrite">If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops.</param> + /// <param name="forceOverwrite">If set, will force to remove existing segments and replace it with new ones otherwise will check for existing segments and if found any that should not be deleted, stops.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task that indicates the Operation is finished.</returns> - Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool overwrite, CancellationToken cancellationToken); + Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool forceOverwrite, CancellationToken cancellationToken); /// <summary> /// Returns if this item supports media segments. diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index e185898bf..f4ac0ece4 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; @@ -102,4 +103,11 @@ public interface IItemRepository IReadOnlyList<string> GetGenreNames(); IReadOnlyList<string> GetAllArtistNames(); + + /// <summary> + /// Checks if an item has been persisted to the database. + /// </summary> + /// <param name="id">The id to check.</param> + /// <returns>True if the item exists, otherwise false.</returns> + Task<bool> ItemExistsAsync(Guid id); } |
