diff options
Diffstat (limited to 'MediaBrowser.Controller')
29 files changed, 832 insertions, 124 deletions
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index b90ec8222..bb0b26b8e 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -25,6 +25,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; @@ -1265,7 +1266,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Overrides the base implementation to refresh metadata for local trailers. + /// The base implementation to refresh metadata. /// </summary> /// <param name="options">The options.</param> /// <param name="cancellationToken">The cancellation token.</param> @@ -1362,9 +1363,7 @@ namespace MediaBrowser.Controller.Entities protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { - var path = ContainingFolderPath; - - return directoryService.GetFileSystemEntries(path); + return directoryService.GetFileSystemEntries(ContainingFolderPath); } private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) @@ -1393,6 +1392,23 @@ namespace MediaBrowser.Controller.Entities return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); }); + // Cleanup removed extras + var removedExtraIds = item.ExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray(); + if (removedExtraIds.Length > 0) + { + var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery() + { + ItemIds = removedExtraIds + }); + foreach (var removedExtra in removedExtras) + { + LibraryManager.DeleteItem(removedExtra, new DeleteOptions() + { + DeleteFileLocation = false + }); + } + } + await Task.WhenAll(tasks).ConfigureAwait(false); item.ExtraIds = newExtraIds; @@ -1407,7 +1423,14 @@ namespace MediaBrowser.Controller.Entities public virtual bool RequiresRefresh() { - return false; + if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue) + { + return false; + } + + var info = FileSystem.GetFileSystemInfo(Path); + + return info.Exists && this.HasChanged(info.LastWriteTimeUtc); } public virtual List<string> GetUserDataKeys() @@ -1970,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 4da22854b..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( @@ -1222,11 +1186,6 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.IsPlayed.HasValue) - { - return false; - } - if (!string.IsNullOrWhiteSpace(request.Person)) { return false; @@ -1267,17 +1226,15 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.MinCommunityRating.HasValue) - { - return false; - } - - if (request.MinCriticRating.HasValue) + if (request.MinIndexNumber.HasValue) { return false; } - if (request.MinIndexNumber.HasValue) + if (request.OrderBy.Any(o => + o.OrderBy == ItemSortBy.CommunityRating || + o.OrderBy == ItemSortBy.CriticRating || + o.OrderBy == ItemSortBy.Runtime)) { return false; } 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/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 46bad3f3b..6bdba36f9 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -7,12 +7,14 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; +using System.Threading; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.TV @@ -22,6 +24,8 @@ namespace MediaBrowser.Controller.Entities.TV /// </summary> public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries { + public static IMediaEncoder MediaEncoder { get; set; } + /// <inheritdoc /> [JsonIgnore] public IReadOnlyList<BaseItem> LocalTrailers => GetExtras() @@ -325,6 +329,39 @@ namespace MediaBrowser.Controller.Entities.TV { if (SourceType == SourceType.Library || SourceType == SourceType.LiveTV) { + var libraryOptions = LibraryManager.GetLibraryOptions(this); + if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(Container, "mp4", StringComparison.OrdinalIgnoreCase)) + { + try + { + var mediaInfo = MediaEncoder.GetMediaInfo( + new MediaInfoRequest + { + MediaSource = GetMediaSources(false)[0], + MediaType = DlnaProfileType.Video + }, + CancellationToken.None).GetAwaiter().GetResult(); + if (mediaInfo.ParentIndexNumber > 0) + { + ParentIndexNumber = mediaInfo.ParentIndexNumber; + } + + if (mediaInfo.IndexNumber > 0) + { + IndexNumber = mediaInfo.IndexNumber; + } + + if (!string.IsNullOrEmpty(mediaInfo.ShowName)) + { + SeriesName = mediaInfo.ShowName; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", Path); + } + } + try { if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata)) 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 c2b4da32a..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(); @@ -924,6 +920,11 @@ namespace MediaBrowser.Controller.Entities } } + if (query.ExcludeItemIds.Contains(item.Id)) + { + return false; + } + return true; } diff --git a/MediaBrowser.Controller/IO/IExternalDataManager.cs b/MediaBrowser.Controller/IO/IExternalDataManager.cs new file mode 100644 index 000000000..f69f4586c --- /dev/null +++ b/MediaBrowser.Controller/IO/IExternalDataManager.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.IO; + +/// <summary> +/// Interface IPathManager. +/// </summary> +public interface IExternalDataManager +{ + /// <summary> + /// Deletes all external item data. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs index 4e4eb514e..eb6743754 100644 --- a/MediaBrowser.Controller/IO/IPathManager.cs +++ b/MediaBrowser.Controller/IO/IPathManager.cs @@ -1,9 +1,10 @@ +using System.Collections.Generic; using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.IO; /// <summary> -/// Interface ITrickplayManager. +/// Interface IPathManager. /// </summary> public interface IPathManager { @@ -60,4 +61,11 @@ public interface IPathManager /// <param name="chapterPositionTicks">The chapter position.</param> /// <returns>The chapter images data path.</returns> public string GetChapterImagePath(BaseItem item, long chapterPositionTicks); + + /// <summary> + /// Gets the paths of extracted data folders. + /// </summary> + /// <param name="item">The base item.</param> + /// <returns>The absolute paths.</returns> + public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item); } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index e9c4d9e19..b76141db0 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -39,6 +39,11 @@ namespace MediaBrowser.Controller string FriendlyName { get; } /// <summary> + /// Gets or sets the path to the backup archive used to restore upon restart. + /// </summary> + string RestoreBackupPath { get; set; } + + /// <summary> /// Gets a URL specific for the request. /// </summary> /// <param name="request">The <see cref="HttpRequest"/> instance.</param> diff --git a/MediaBrowser.Controller/Library/IKeyframeManager.cs b/MediaBrowser.Controller/Library/IKeyframeManager.cs new file mode 100644 index 000000000..b0155efdd --- /dev/null +++ b/MediaBrowser.Controller/Library/IKeyframeManager.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.MediaEncoding.Keyframes; + +namespace MediaBrowser.Controller.IO; + +/// <summary> +/// Interface IKeyframeManager. +/// </summary> +public interface IKeyframeManager +{ + /// <summary> + /// Gets the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <returns>The keyframe data.</returns> + IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId); + + /// <summary> + /// Saves the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="data">The keyframe data.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The task object representing the asynchronous operation.</returns> + Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken); + + /// <summary> + /// Deletes the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The task object representing the asynchronous operation.</returns> + Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index df90f546c..98ed15eb6 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -220,13 +220,13 @@ namespace MediaBrowser.Controller.Library /// <param name="resolvers">The resolvers.</param> /// <param name="introProviders">The intro providers.</param> /// <param name="itemComparers">The item comparers.</param> - /// <param name="postscanTasks">The postscan tasks.</param> + /// <param name="postScanTasks">The post scan tasks.</param> void AddParts( IEnumerable<IResolverIgnoreRule> rules, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, IEnumerable<IBaseItemComparer> itemComparers, - IEnumerable<ILibraryPostScanTask> postscanTasks); + IEnumerable<ILibraryPostScanTask> postScanTasks); /// <summary> /// Sorts the specified items. @@ -593,11 +593,11 @@ namespace MediaBrowser.Controller.Library QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query); /// <summary> - /// Ignores the file. + /// Checks if the file is ignored. /// </summary> /// <param name="file">The file.</param> /// <param name="parent">The parent.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + /// <returns><c>true</c> if ignored, <c>false</c> otherwise.</returns> bool IgnoreFile(FileSystemMetadata file, BaseItem parent); Guid GetStudioId(string name); 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 7c3138002..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; } @@ -1572,6 +1572,26 @@ namespace MediaBrowser.Controller.MediaEncoding return FormattableString.Invariant($" -maxrate {bitrate} -bufsize {bufsize}"); } + if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "av1_qsv", StringComparison.OrdinalIgnoreCase)) + { + // TODO: probe QSV encoders' capabilities and enable more tuning options + // See also https://github.com/intel/media-delivery/blob/master/doc/quality.rst + + // Enable MacroBlock level bitrate control for better subjective visual quality + var mbbrcOpt = string.Empty; + if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) + { + mbbrcOpt = " -mbbrc 1"; + } + + // Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation + // Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes + return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}"); + } + if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase)) @@ -2356,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) @@ -2363,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. @@ -3471,6 +3504,21 @@ namespace MediaBrowser.Controller.MediaEncoding doubleRateDeint ? "1" : "0"); } + if (hwDeintSuffix.Contains("opencl", StringComparison.OrdinalIgnoreCase)) + { + var useBwdif = options.DeinterlaceMethod == DeinterlaceMethod.bwdif; + + if (_mediaEncoder.SupportsFilter("yadif_opencl") + && _mediaEncoder.SupportsFilter("bwdif_opencl")) + { + return string.Format( + CultureInfo.InvariantCulture, + "{0}_opencl={1}:-1:0", + useBwdif ? "bwdif" : "yadif", + doubleRateDeint ? "1" : "0"); + } + } + if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) { return string.Format( @@ -3947,6 +3995,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasSubs) { + var alphaFormatOpt = string.Empty; if (hasGraphicalSubs) { var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); @@ -3964,10 +4013,13 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add(alphaSrcFilter); subFilters.Add("format=yuva420p"); subFilters.Add(subTextSubtitlesFilter); + + alphaFormatOpt = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayCudaAlphaFormat) + ? ":alpha_format=premultiplied" : string.Empty; } subFilters.Add("hwupload=derive_device=cuda"); - overlayFilters.Add("overlay_cuda=eof_action=pass:repeatlast=0"); + overlayFilters.Add($"overlay_cuda=eof_action=pass:repeatlast=0{alphaFormatOpt}"); } } else @@ -4099,7 +4151,12 @@ namespace MediaBrowser.Controller.MediaEncoding // map from d3d11va to opencl via d3d11-opencl interop. mainFilters.Add("hwmap=derive_device=opencl:mode=read"); - // hw deint <= TODO: finish the 'yadif_opencl' filter + // hw deint + if (doDeintH2645) + { + var deintFilter = GetHwDeinterlaceFilter(state, options, "opencl"); + mainFilters.Add(deintFilter); + } // hw transpose if (doOclTranspose) @@ -4164,6 +4221,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasSubs) { + var alphaFormatOpt = string.Empty; if (hasGraphicalSubs) { var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); @@ -4181,10 +4239,13 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add(alphaSrcFilter); subFilters.Add("format=yuva420p"); subFilters.Add(subTextSubtitlesFilter); + + alphaFormatOpt = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayOpenclAlphaFormat) + ? ":alpha_format=premultiplied" : string.Empty; } subFilters.Add("hwupload=derive_device=opencl"); - overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0"); + overlayFilters.Add($"overlay_opencl=eof_action=pass:repeatlast=0{alphaFormatOpt}"); overlayFilters.Add("hwmap=derive_device=d3d11va:mode=write:reverse=1"); overlayFilters.Add("format=d3d11"); } @@ -4387,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}"; @@ -5892,7 +5960,7 @@ namespace MediaBrowser.Controller.MediaEncoding // 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 hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_divisible_by=4:afbc=1"; + 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); } @@ -5970,9 +6038,10 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasSubs) { + var subMaxH = 1080; if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, subMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5982,7 +6051,7 @@ namespace MediaBrowser.Controller.MediaEncoding var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; // alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload - var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, subMaxH, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=bgra"); @@ -5991,6 +6060,13 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add("hwupload=derive_device=rkmpp"); + // offload 1080p+ subtitles swscale upscaling from CPU to RGA + var (overlayW, overlayH) = GetFixedOutputSize(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + if (overlayW.HasValue && overlayH.HasValue && overlayH.Value > subMaxH) + { + subFilters.Add($"vpp_rkrga=w={overlayW.Value}:h={overlayH.Value}:format=bgra:afbc=1"); + } + // try enabling AFBC to save DDR bandwidth var hwOverlayFilter = "overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12"; if (isEncoderSupportAfbc) @@ -6947,7 +7023,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "av1", bitDepth, hwSurface); + var accelType = GetHwaccelType(state, options, "av1", bitDepth, hwSurface); + return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty); } } @@ -7074,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/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs index a2b6e1d73..6ad953023 100644 --- a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs +++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs @@ -38,6 +38,16 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// The transpose_opencl_reversal. /// </summary> - TransposeOpenclReversal = 6 + TransposeOpenclReversal = 6, + + /// <summary> + /// The overlay_opencl_alpha_format. + /// </summary> + OverlayOpenclAlphaFormat = 7, + + /// <summary> + /// The overlay_cuda_alpha_format. + /// </summary> + OverlayCudaAlphaFormat = 8 } } diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs index 456977b88..4f13a7ecc 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs @@ -5,9 +5,10 @@ using System.Threading.Tasks; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.MediaSegments; -namespace MediaBrowser.Controller; +namespace MediaBrowser.Controller.MediaSegments; /// <summary> /// Defines methods for interacting with media segments. @@ -18,10 +19,11 @@ public interface IMediaSegmentManager /// Uses all segment providers enabled for the <see cref="BaseItem"/>'s library to get the Media Segments. /// </summary> /// <param name="baseItem">The Item to evaluate.</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="cancellationToken">stop request token.</param> + /// <param name="libraryOptions">The library options.</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, bool overwrite, CancellationToken cancellationToken); + Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool forceOverwrite, CancellationToken cancellationToken); /// <summary> /// Returns if this item supports media segments. @@ -46,22 +48,22 @@ public interface IMediaSegmentManager Task DeleteSegmentAsync(Guid segmentId); /// <summary> - /// Obtains all segments associated with the itemId. + /// Deletes all media segments of an item. /// </summary> - /// <param name="itemId">The id of the <see cref="BaseItem"/>.</param> - /// <param name="typeFilter">filters all media segments of the given type to be included. If null all types are included.</param> - /// <param name="filterByProvider">When set filters the segments to only return those that which providers are currently enabled on their library.</param> - /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns> - Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true); + /// <param name="itemId">The <see cref="BaseItem.Id"/> to delete all segments for.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>a task.</returns> + Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken); /// <summary> /// Obtains all segments associated with the itemId. /// </summary> /// <param name="item">The <see cref="BaseItem"/>.</param> /// <param name="typeFilter">filters all media segments of the given type to be included. If null all types are included.</param> + /// <param name="libraryOptions">The library options.</param> /// <param name="filterByProvider">When set filters the segments to only return those that which providers are currently enabled on their library.</param> /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns> - Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true); + Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, LibraryOptions libraryOptions, bool filterByProvider = true); /// <summary> /// Gets information about any media segments stored for the given itemId. diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs index 39bb58bef..5a6d15d78 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model; using MediaBrowser.Model.MediaSegments; -namespace MediaBrowser.Controller; +namespace MediaBrowser.Controller.MediaSegments; /// <summary> /// Provides methods for Obtaining the Media Segments from an Item. 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); } diff --git a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs index 4930434a7..2596784ba 100644 --- a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs +++ b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs @@ -26,4 +26,12 @@ public interface IKeyframeRepository /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The task object representing the asynchronous operation.</returns> Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken); + + /// <summary> + /// Deletes the keyframe data. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The task object representing the asynchronous operation.</returns> + Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken); } diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs index 97f653edf..2206a021a 100644 --- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -26,6 +26,6 @@ namespace MediaBrowser.Controller.Sorting /// Gets or sets the user data repository. /// </summary> /// <value>The user data repository.</value> - IUserDataManager UserDataRepository { get; set; } + IUserDataManager UserDataManager { get; set; } } } diff --git a/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs new file mode 100644 index 000000000..b094ec275 --- /dev/null +++ b/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs @@ -0,0 +1,34 @@ +using System; + +namespace MediaBrowser.Controller.SystemBackupService; + +/// <summary> +/// Manifest type for backups internal structure. +/// </summary> +public class BackupManifestDto +{ + /// <summary> + /// Gets or sets the jellyfin version this backup was created with. + /// </summary> + public required Version ServerVersion { get; set; } + + /// <summary> + /// Gets or sets the backup engine version this backup was created with. + /// </summary> + public required Version BackupEngineVersion { get; set; } + + /// <summary> + /// Gets or sets the date this backup was created with. + /// </summary> + public required DateTimeOffset DateCreated { get; set; } + + /// <summary> + /// Gets or sets the path to the backup on the system. + /// </summary> + public required string Path { get; set; } + + /// <summary> + /// Gets or sets the contents of the backup archive. + /// </summary> + public required BackupOptionsDto Options { get; set; } +} diff --git a/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs new file mode 100644 index 000000000..fc5a109f1 --- /dev/null +++ b/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs @@ -0,0 +1,29 @@ +using System; + +namespace MediaBrowser.Controller.SystemBackupService; + +/// <summary> +/// Defines the optional contents of the backup archive. +/// </summary> +public class BackupOptionsDto +{ + /// <summary> + /// Gets or sets a value indicating whether the archive contains the Metadata contents. + /// </summary> + public bool Metadata { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the archive contains the Trickplay contents. + /// </summary> + public bool Trickplay { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the archive contains the Subtitle contents. + /// </summary> + public bool Subtitles { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the archive contains the Database contents. + /// </summary> + public bool Database { get; set; } = true; +} diff --git a/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs new file mode 100644 index 000000000..263fa00c8 --- /dev/null +++ b/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs @@ -0,0 +1,15 @@ +using System; +using MediaBrowser.Common.Configuration; + +namespace MediaBrowser.Controller.SystemBackupService; + +/// <summary> +/// Defines properties used to start a restore process. +/// </summary> +public class BackupRestoreRequestDto +{ + /// <summary> + /// Gets or Sets the name of the backup archive to restore from. Must be present in <see cref="IApplicationPaths.BackupPath"/>. + /// </summary> + public required string ArchiveFileName { get; set; } +} diff --git a/MediaBrowser.Controller/SystemBackupService/IBackupService.cs b/MediaBrowser.Controller/SystemBackupService/IBackupService.cs new file mode 100644 index 000000000..0c586d811 --- /dev/null +++ b/MediaBrowser.Controller/SystemBackupService/IBackupService.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Controller.SystemBackupService; + +namespace Jellyfin.Server.Implementations.SystemBackupService; + +/// <summary> +/// Defines an interface to restore and backup the jellyfin system. +/// </summary> +public interface IBackupService +{ + /// <summary> + /// Creates a new Backup zip file containing the current state of the application. + /// </summary> + /// <param name="backupOptions">The backup options.</param> + /// <returns>A task.</returns> + Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions); + + /// <summary> + /// Gets a list of backups that are available to be restored from. + /// </summary> + /// <returns>A list of backup paths.</returns> + Task<BackupManifestDto[]> EnumerateBackups(); + + /// <summary> + /// Gets a single backup manifest if the path defines a valid Jellyfin backup archive. + /// </summary> + /// <param name="archivePath">The path to be loaded.</param> + /// <returns>The containing backup manifest or null if not existing or compatiable.</returns> + Task<BackupManifestDto?> GetBackupManifest(string archivePath); + + /// <summary> + /// Restores an backup zip file created by jellyfin. + /// </summary> + /// <param name="archivePath">Path to the archive.</param> + /// <returns>A Task.</returns> + /// <exception cref="FileNotFoundException">Thrown when an invalid or missing file is specified.</exception> + /// <exception cref="NotSupportedException">Thrown when attempt to load an unsupported backup is made.</exception> + /// <exception cref="InvalidOperationException">Thrown for errors during the restore.</exception> + Task RestoreBackupAsync(string archivePath); + + /// <summary> + /// Schedules a Restore and restarts the server. + /// </summary> + /// <param name="archivePath">The path to the archive to restore from.</param> + void ScheduleRestoreAndRestartServer(string archivePath); +} diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 9ac8ead11..fba24329a 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -21,7 +21,7 @@ public interface ITrickplayManager /// <param name="libraryOptions">The library options.</param> /// <param name="cancellationToken">CancellationToken to use for operation.</param> /// <returns>Task.</returns> - Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken); + Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions libraryOptions, CancellationToken cancellationToken); /// <summary> /// Creates trickplay tiles out of individual thumbnails. @@ -59,6 +59,14 @@ public interface ITrickplayManager Task SaveTrickplayInfo(TrickplayInfo info); /// <summary> + /// Deletes all trickplay info for an item. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task DeleteTrickplayDataAsync(Guid itemId, CancellationToken cancellationToken); + + /// <summary> /// Gets all trickplay infos for all media streams of an item. /// </summary> /// <param name="item">The item.</param> @@ -93,7 +101,7 @@ public interface ITrickplayManager /// <param name="libraryOptions">The library options.</param> /// <param name="cancellationToken">CancellationToken to use for operation.</param> /// <returns>Task.</returns> - Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken); + Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions libraryOptions, CancellationToken cancellationToken); /// <summary> /// Gets the trickplay HLS playlist. |
