aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs2
-rw-r--r--MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs5
-rw-r--r--MediaBrowser.Controller/Chapters/IChapterManager.cs57
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs118
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs14
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs314
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs12
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs6
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs39
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs4
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs7
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs10
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs45
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs32
-rw-r--r--MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs8
-rw-r--r--MediaBrowser.Controller/IDisplayPreferencesManager.cs11
-rw-r--r--MediaBrowser.Controller/IO/FileSystemHelper.cs79
-rw-r--r--MediaBrowser.Controller/IO/IExternalDataManager.cs19
-rw-r--r--MediaBrowser.Controller/IO/IPathManager.cs26
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs5
-rw-r--r--MediaBrowser.Controller/IServerApplicationPaths.cs4
-rw-r--r--MediaBrowser.Controller/Library/IKeyframeManager.cs37
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs19
-rw-r--r--MediaBrowser.Controller/LibraryTaskScheduler/ILimitedConcurrencyLibraryScheduler.cs23
-rw-r--r--MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs335
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs102
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs20
-rw-r--r--MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs12
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs28
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs8
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs24
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs6
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs27
-rw-r--r--MediaBrowser.Controller/Persistence/IChapterRepository.cs (renamed from MediaBrowser.Controller/Chapters/IChapterRepository.cs)30
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs32
-rw-r--r--MediaBrowser.Controller/Persistence/IKeyframeRepository.cs8
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs6
-rw-r--r--MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs2
-rw-r--r--MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs34
-rw-r--r--MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs29
-rw-r--r--MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs15
-rw-r--r--MediaBrowser.Controller/SystemBackupService/IBackupService.cs48
-rw-r--r--MediaBrowser.Controller/Trickplay/ITrickplayManager.cs12
45 files changed, 1357 insertions, 321 deletions
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
index 976a667ac..c993ceea8 100644
--- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
@@ -14,8 +14,6 @@ namespace MediaBrowser.Controller.Authentication
Task<ProviderAuthenticationResult> Authenticate(string username, string password);
- bool HasPassword(User user);
-
Task ChangePassword(User user, string newPassword);
}
diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
index 592ce9955..36cd5c5d1 100644
--- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -15,11 +13,12 @@ namespace MediaBrowser.Controller.Authentication
bool IsEnabled { get; }
- Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork);
+ Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork);
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
}
+#nullable disable
public class PasswordPinCreationResult
{
public string PinFile { get; set; }
diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs
new file mode 100644
index 000000000..25656fd62
--- /dev/null
+++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Chapters;
+
+/// <summary>
+/// Interface IChapterManager.
+/// </summary>
+public interface IChapterManager
+{
+ /// <summary>
+ /// Saves the chapters.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="chapters">The set of chapters.</param>
+ void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters);
+
+ /// <summary>
+ /// Gets a single chapter of a BaseItem on a specific index.
+ /// </summary>
+ /// <param name="baseItemId">The BaseItems id.</param>
+ /// <param name="index">The index of that chapter.</param>
+ /// <returns>A chapter instance.</returns>
+ ChapterInfo? GetChapter(Guid baseItemId, int index);
+
+ /// <summary>
+ /// Gets all chapters associated with the baseItem.
+ /// </summary>
+ /// <param name="baseItemId">The BaseItems id.</param>
+ /// <returns>A readonly list of chapter instances.</returns>
+ IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId);
+
+ /// <summary>
+ /// Refreshes the chapter images.
+ /// </summary>
+ /// <param name="video">Video to use.</param>
+ /// <param name="directoryService">Directory service to use.</param>
+ /// <param name="chapters">Set of chapters to refresh.</param>
+ /// <param name="extractImages">Option to extract images.</param>
+ /// <param name="saveChapters">Option to save chapters.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns><c>true</c> if successful, <c>false</c> if not.</returns>
+ Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Deletes the chapter data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index d1a6b3584..3c46d53e5 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -24,7 +24,9 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
@@ -34,7 +36,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities
@@ -107,8 +108,15 @@ namespace MediaBrowser.Controller.Entities
ProductionLocations = Array.Empty<string>();
RemoteTrailers = Array.Empty<MediaUrl>();
ExtraIds = Array.Empty<Guid>();
+ UserData = [];
}
+ /// <summary>
+ /// Gets or Sets the user data collection as cached from the last Db query.
+ /// </summary>
+ [JsonIgnore]
+ public ICollection<UserData> UserData { get; set; }
+
[JsonIgnore]
public string PreferredMetadataCountryCode { get; set; }
@@ -484,7 +492,7 @@ namespace MediaBrowser.Controller.Entities
public static IItemRepository ItemRepository { get; set; }
- public static IChapterRepository ChapterRepository { get; set; }
+ public static IChapterManager ChapterManager { get; set; }
public static IFileSystem FileSystem { get; set; }
@@ -701,19 +709,7 @@ namespace MediaBrowser.Controller.Entities
{
get
{
- var customRating = CustomRating;
- if (!string.IsNullOrEmpty(customRating))
- {
- return customRating;
- }
-
- var parent = DisplayParent;
- if (parent is not null)
- {
- return parent.CustomRatingForComparison;
- }
-
- return null;
+ return GetCustomRatingForComparision();
}
}
@@ -791,6 +787,26 @@ namespace MediaBrowser.Controller.Entities
/// <value>The remote trailers.</value>
public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
+ private string GetCustomRatingForComparision(HashSet<Guid> callstack = null)
+ {
+ callstack ??= new();
+ var customRating = CustomRating;
+ if (!string.IsNullOrEmpty(customRating))
+ {
+ return customRating;
+ }
+
+ callstack.Add(Id);
+
+ var parent = DisplayParent;
+ if (parent is not null && !callstack.Contains(parent.Id))
+ {
+ return parent.GetCustomRatingForComparision(callstack);
+ }
+
+ return null;
+ }
+
public virtual double GetDefaultPrimaryImageAspectRatio()
{
return 0;
@@ -1112,6 +1128,15 @@ namespace MediaBrowser.Controller.Entities
var protocol = item.PathProtocol;
+ // Resolve the item path so everywhere we use the media source it will always point to
+ // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
+ // path will return null, so it's safe to check for all paths.
+ var itemPath = item.Path;
+ if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo)
+ {
+ itemPath = linkInfo.FullName;
+ }
+
var info = new MediaSourceInfo
{
Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
@@ -1119,7 +1144,7 @@ namespace MediaBrowser.Controller.Entities
MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
Name = GetMediaSourceName(item),
- Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path,
+ Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
RunTimeTicks = item.RunTimeTicks,
Container = item.Container,
Size = item.Size,
@@ -1266,7 +1291,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>
@@ -1363,9 +1388,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)
@@ -1394,6 +1417,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;
@@ -1408,7 +1448,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()
@@ -1805,7 +1852,7 @@ namespace MediaBrowser.Controller.Entities
public void SetStudios(IEnumerable<string> names)
{
- Studios = names.Trimmed().Distinct().ToArray();
+ Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
/// <summary>
@@ -1971,9 +2018,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
@@ -2051,7 +2099,7 @@ namespace MediaBrowser.Controller.Entities
{
if (imageType == ImageType.Chapter)
{
- var chapter = ChapterRepository.GetChapter(this.Id, imageIndex);
+ var chapter = ChapterManager.GetChapter(Id, imageIndex);
if (chapter is null)
{
@@ -2101,7 +2149,7 @@ namespace MediaBrowser.Controller.Entities
if (image.Type == ImageType.Chapter)
{
- var chapters = ChapterRepository.GetChapters(this.Id);
+ var chapters = ChapterManager.GetChapters(Id);
for (var i = 0; i < chapters.Count; i++)
{
if (chapters[i].ImagePath == image.Path)
@@ -2284,27 +2332,27 @@ namespace MediaBrowser.Controller.Entities
return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None);
}
- public virtual bool IsPlayed(User user)
+ public virtual bool IsPlayed(User user, UserItemData userItemData)
{
- var userdata = UserDataManager.GetUserData(user, this);
+ userItemData ??= UserDataManager.GetUserData(user, this);
- return userdata is not null && userdata.Played;
+ return userItemData is not null && userItemData.Played;
}
- public bool IsFavoriteOrLiked(User user)
+ public bool IsFavoriteOrLiked(User user, UserItemData userItemData)
{
- var userdata = UserDataManager.GetUserData(user, this);
+ userItemData ??= UserDataManager.GetUserData(user, this);
- return userdata is not null && (userdata.IsFavorite || (userdata.Likes ?? false));
+ return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false));
}
- public virtual bool IsUnplayed(User user)
+ public virtual bool IsUnplayed(User user, UserItemData userItemData)
{
ArgumentNullException.ThrowIfNull(user);
- var userdata = UserDataManager.GetUserData(user, this);
+ userItemData ??= UserDataManager.GetUserData(user, this);
- return userdata is null || !userdata.Played;
+ return userItemData is null || !userItemData.Played;
}
ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
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..151b957fe 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;
@@ -42,6 +42,8 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Folder : BaseItem
{
+ private IEnumerable<BaseItem> _children;
+
public Folder()
{
LinkedChildren = Array.Empty<LinkedChild>();
@@ -49,6 +51,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>
@@ -106,11 +110,15 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Gets the actual children.
+ /// Gets or Sets the actual children.
/// </summary>
/// <value>The actual children.</value>
[JsonIgnore]
- public virtual IEnumerable<BaseItem> Children => LoadChildren();
+ public virtual IEnumerable<BaseItem> Children
+ {
+ get => _children ??= LoadChildren();
+ set => _children = value;
+ }
/// <summary>
/// Gets thread-safe access to all recursive children of this folder - without regard to user.
@@ -279,6 +287,7 @@ namespace MediaBrowser.Controller.Entities
/// <returns>Task.</returns>
public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, bool allowRemoveRoot = false, CancellationToken cancellationToken = default)
{
+ Children = null; // invalidate cached children.
return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
}
@@ -286,6 +295,7 @@ namespace MediaBrowser.Controller.Entities
{
var dictionary = new Dictionary<Guid, BaseItem>();
+ Children = null; // invalidate cached children.
var childrenList = Children.ToList();
foreach (var child in childrenList)
@@ -327,6 +337,11 @@ namespace MediaBrowser.Controller.Entities
try
{
+ if (GetParents().Any(f => f.Id.Equals(Id)))
+ {
+ throw new InvalidOperationException("Recursive datastructure detected abort processing this item.");
+ }
+
await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
}
finally
@@ -442,6 +457,12 @@ namespace MediaBrowser.Controller.Entities
{
foreach (var item in itemsRemoved)
{
+ if (!item.CanDelete())
+ {
+ Logger.LogDebug("Item marked as non-removable, skipping: {Path}", item.Path ?? item.Name);
+ continue;
+ }
+
if (item.IsFileProtocol)
{
Logger.LogDebug("Removed item: {Path}", item.Path);
@@ -524,6 +545,7 @@ namespace MediaBrowser.Controller.Entities
{
if (validChildrenNeedGeneration)
{
+ Children = null; // invalidate cached children.
validChildren = Children.ToList();
}
@@ -566,7 +588,8 @@ namespace MediaBrowser.Controller.Entities
if (recursive && child is Folder folder)
{
- await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
+ folder.Children = null; // invalidate cached children.
+ await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -598,51 +621,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>
@@ -722,16 +707,29 @@ namespace MediaBrowser.Controller.Entities
IEnumerable<BaseItem> items;
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
+ var totalCount = 0;
if (query.User is null)
{
items = GetRecursiveChildren(filter);
+ totalCount = items.Count();
}
else
{
- items = GetRecursiveChildren(user, query);
+ // Save pagination params before clearing them to prevent pagination from happening
+ // before sorting. PostFilterAndSort will apply pagination after sorting.
+ var limit = query.Limit;
+ var startIndex = query.StartIndex;
+ query.Limit = null;
+ query.StartIndex = null;
+
+ items = GetRecursiveChildren(user, query, out totalCount);
+
+ // Restore pagination params so PostFilterAndSort can apply them after sorting
+ query.Limit = limit;
+ query.StartIndex = startIndex;
}
- return PostFilterAndSort(items, query, true);
+ return PostFilterAndSort(items, query);
}
if (this is not UserRootFolder
@@ -980,25 +978,31 @@ namespace MediaBrowser.Controller.Entities
IEnumerable<BaseItem> items;
+ int totalItemCount = 0;
if (query.User is null)
{
items = Children.Where(filter);
+ totalItemCount = items.Count();
}
else
{
// need to pass this param to the children.
+ // Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
var childQuery = new InternalItemsQuery
{
- DisplayAlbumFolders = query.DisplayAlbumFolders
+ DisplayAlbumFolders = query.DisplayAlbumFolders,
+ NameStartsWith = query.NameStartsWith,
+ NameStartsWithOrGreater = query.NameStartsWithOrGreater,
+ NameLessThan = query.NameLessThan
};
- items = GetChildren(user, true, childQuery).Where(filter);
+ items = GetChildren(user, true, out totalItemCount, 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 +1012,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 +1027,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 +1035,15 @@ namespace MediaBrowser.Controller.Entities
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
}
- return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
+ var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList();
+ var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager);
+
+ if (query.EnableTotalRecordCount)
+ {
+ result.TotalRecordCount = filteredItems.Count;
+ }
+
+ return result;
}
private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
@@ -1044,12 +1056,49 @@ namespace MediaBrowser.Controller.Entities
{
ArgumentNullException.ThrowIfNull(items);
- if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
+ if (!CollapseBoxSetItems(query, queryParent, user, configurationManager))
+ {
+ return items;
+ }
+
+ var config = configurationManager.Configuration;
+
+ bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
+ bool collapseSeries = config.EnableGroupingShowsIntoCollections;
+
+ if (user is null || (collapseMovies && collapseSeries))
+ {
+ return collectionManager.CollapseItemsWithinBoxSets(items, user);
+ }
+
+ if (!collapseMovies && !collapseSeries)
{
- items = collectionManager.CollapseItemsWithinBoxSets(items, user);
+ return items;
}
- return items;
+ var collapsibleItems = new List<BaseItem>();
+ var remainingItems = new List<BaseItem>();
+
+ foreach (var item in items)
+ {
+ if ((collapseMovies && item is Movie) || (collapseSeries && item is Series))
+ {
+ collapsibleItems.Add(item);
+ }
+ else
+ {
+ remainingItems.Add(item);
+ }
+ }
+
+ if (collapsibleItems.Count == 0)
+ {
+ return remainingItems;
+ }
+
+ var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user);
+
+ return collapsedItems.Concat(remainingItems);
}
private static bool CollapseBoxSetItems(
@@ -1080,24 +1129,26 @@ namespace MediaBrowser.Controller.Entities
}
var param = query.CollapseBoxSetItems;
-
- if (!param.HasValue)
+ if (param.HasValue)
{
- if (user is not null && query.IncludeItemTypes.Any(type =>
- (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) ||
- (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections)))
- {
- return false;
- }
+ return param.Value && AllowBoxSetCollapsing(query);
+ }
- if (query.IncludeItemTypes.Length == 0
- || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series))
- {
- param = true;
- }
+ var config = configurationManager.Configuration;
+
+ bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie);
+ bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Series);
+
+ bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
+ bool collapseSeries = config.EnableGroupingShowsIntoCollections;
+
+ if (user is not null)
+ {
+ bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries);
+ return canCollapse && AllowBoxSetCollapsing(query);
}
- return param.HasValue && param.Value && AllowBoxSetCollapsing(query);
+ return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query);
}
private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
@@ -1222,11 +1273,6 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.IsPlayed.HasValue)
- {
- return false;
- }
-
if (!string.IsNullOrWhiteSpace(request.Person))
{
return false;
@@ -1267,17 +1313,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;
}
@@ -1285,30 +1329,30 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- public IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren)
- {
- ArgumentNullException.ThrowIfNull(user);
-
- return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user));
- }
-
- public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null)
{
ArgumentNullException.ThrowIfNull(user);
+ query ??= new InternalItemsQuery();
+ query.User = user;
// the true root should return our users root folder children
if (IsPhysicalRoot)
{
- return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren);
+ return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren, out totalItemCount);
}
var result = new Dictionary<Guid, BaseItem>();
- AddChildren(user, includeLinkedChildren, result, false, query);
+ totalItemCount = AddChildren(user, includeLinkedChildren, result, false, query);
return result.Values.ToArray();
}
+ public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query = null)
+ {
+ return GetChildren(user, includeLinkedChildren, out _, query);
+ }
+
protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return Children;
@@ -1317,13 +1361,13 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Adds the children to list.
/// </summary>
- private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null)
+ private int AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null)
{
// Prevent infinite recursion of nested folders
visitedFolders ??= new HashSet<Folder>();
if (!visitedFolders.Add(this))
{
- return;
+ return 0;
}
// If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
@@ -1340,44 +1384,67 @@ namespace MediaBrowser.Controller.Entities
children = GetEligibleChildrenForRecursiveChildren(user);
}
- AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
-
if (includeLinkedChildren)
{
- AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query, visitedFolders);
+ children = children.Concat(GetLinkedChildren(user)).ToArray();
}
+
+ return AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
}
- private void AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders)
+ private int AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders)
{
- foreach (var child in children)
+ query ??= new InternalItemsQuery();
+ var limit = query.Limit > 0 ? query.Limit : int.MaxValue;
+ query.Limit = 0;
+
+ var visibleChildren = children
+ .Where(e => e.IsVisible(user))
+ .ToArray();
+
+ var realChildren = visibleChildren
+ .Where(e => query is null || UserViewBuilder.FilterItem(e, query))
+ .ToArray();
+
+ if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0))
{
- if (!child.IsVisible(user))
- {
- continue;
- }
+ realChildren = realChildren
+ .OrderBy(e => e.ProductionYear ?? int.MaxValue)
+ .ToArray();
+ }
- if (query is null || UserViewBuilder.FilterItem(child, query))
+ var childCount = realChildren.Length;
+ if (result.Count < limit)
+ {
+ var remainingCount = (int)(limit - result.Count);
+ foreach (var child in realChildren
+ .Skip(query.StartIndex ?? 0)
+ .Take(remainingCount))
{
result[child.Id] = child;
}
+ }
- if (recursive && child.IsFolder)
+ if (recursive)
+ {
+ foreach (var child in visibleChildren
+ .Where(e => e.IsFolder)
+ .OfType<Folder>())
{
- var folder = (Folder)child;
-
- folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
+ childCount += child.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
}
}
+
+ return childCount;
}
- public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{
ArgumentNullException.ThrowIfNull(user);
var result = new Dictionary<Guid, BaseItem>();
- AddChildren(user, true, result, true, query);
+ totalCount = AddChildren(user, true, result, true, query);
return result.Values.ToArray();
}
@@ -1709,23 +1776,14 @@ namespace MediaBrowser.Controller.Entities
}
}
- public override bool IsPlayed(User user)
+ public override bool IsPlayed(User user, UserItemData userItemData)
{
- var itemsResult = GetItemList(new InternalItemsQuery(user)
- {
- Recursive = true,
- IsFolder = false,
- IsVirtualItem = false,
- EnableTotalRecordCount = false
- });
-
- return itemsResult
- .All(i => i.IsPlayed(user));
+ return ItemRepository.GetIsPlayed(user, Id, true);
}
- public override bool IsUnplayed(User user)
+ public override bool IsUnplayed(User user, UserItemData userItemData)
{
- return !IsPlayed(user);
+ return !IsPlayed(user, userItemData);
}
public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
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/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index d656fccb4..1d1fb2c39 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -136,9 +136,9 @@ namespace MediaBrowser.Controller.Entities.Movies
return Sort(children, user).ToArray();
}
- public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{
- var children = base.GetRecursiveChildren(user, query);
+ var children = base.GetRecursiveChildren(user, query, out totalCount);
return Sort(children, user).ToArray();
}
@@ -197,7 +197,7 @@ namespace MediaBrowser.Controller.Entities.Movies
var expandedFolders = new List<Guid>();
return FlattenItems(this, expandedFolders)
- .SelectMany(i => LibraryManager.GetCollectionFolders(i))
+ .SelectMany(LibraryManager.GetCollectionFolders)
.Select(i => i.Id)
.Distinct()
.ToArray();
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..b972ebaa6 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -123,7 +123,7 @@ namespace MediaBrowser.Controller.Entities.TV
public override int GetChildCount(User user)
{
- var result = GetChildren(user, true).Count;
+ var result = GetChildren(user, true, null).Count;
return result;
}
@@ -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..427c2995b 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)
{
@@ -301,6 +297,7 @@ namespace MediaBrowser.Controller.Entities.TV
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
+ Children = null; // invalidate cached children.
// Refresh bottom up, seasons and episodes first, then the series
var items = GetRecursiveChildren();
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/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index dfa31315c..5624f8b2e 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.Entities
/// <inheritdoc />
public override int GetChildCount(User user)
{
- return GetChildren(user, true).Count;
+ return GetChildren(user, true, null).Count;
}
/// <inheritdoc />
@@ -134,20 +134,22 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
- public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{
query.SetUser(user);
query.Recursive = true;
query.EnableTotalRecordCount = false;
query.ForceDirect = true;
+ var data = GetItemList(query);
+ totalCount = data.Count;
- return GetItemList(query);
+ return data;
}
/// <inheritdoc />
protected override IReadOnlyList<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
- return GetChildren(user, false);
+ return GetChildren(user, false, null);
}
public static bool IsUserSpecific(Folder folder)
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index c2b4da32a..4f9e9261b 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();
@@ -476,6 +472,23 @@ namespace MediaBrowser.Controller.Entities
public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager)
{
+ if (!string.IsNullOrEmpty(query.NameStartsWith) && !item.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase))
+ {
+ return false;
+ }
+
+#pragma warning disable CA1309 // Use ordinal string comparison
+ if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater) && string.Compare(query.NameStartsWithOrGreater, item.SortName, StringComparison.InvariantCultureIgnoreCase) == 1)
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrEmpty(query.NameLessThan) && string.Compare(query.NameLessThan, item.SortName, StringComparison.InvariantCultureIgnoreCase) != 1)
+#pragma warning restore CA1309 // Use ordinal string comparison
+ {
+ return false;
+ }
+
if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType))
{
return false;
@@ -506,7 +519,6 @@ namespace MediaBrowser.Controller.Entities
if (query.IsLiked.HasValue)
{
userData = userDataManager.GetUserData(user, item);
-
if (!userData.Likes.HasValue || userData.Likes != query.IsLiked.Value)
{
return false;
@@ -515,7 +527,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsFavoriteOrLiked.HasValue)
{
- userData = userData ?? userDataManager.GetUserData(user, item);
+ userData ??= userDataManager.GetUserData(user, item);
var isFavoriteOrLiked = userData.IsFavorite || (userData.Likes ?? false);
if (isFavoriteOrLiked != query.IsFavoriteOrLiked.Value)
@@ -526,8 +538,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsFavorite.HasValue)
{
- userData = userData ?? userDataManager.GetUserData(user, item);
-
+ userData ??= userDataManager.GetUserData(user, item);
if (userData.IsFavorite != query.IsFavorite.Value)
{
return false;
@@ -536,7 +547,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsResumable.HasValue)
{
- userData = userData ?? userDataManager.GetUserData(user, item);
+ userData ??= userDataManager.GetUserData(user, item);
var isResumable = userData.PlaybackPositionTicks > 0;
if (isResumable != query.IsResumable.Value)
@@ -547,7 +558,8 @@ namespace MediaBrowser.Controller.Entities
if (query.IsPlayed.HasValue)
{
- if (item.IsPlayed(user) != query.IsPlayed.Value)
+ userData ??= userDataManager.GetUserData(user, item);
+ if (item.IsPlayed(user, userData) != query.IsPlayed.Value)
{
return false;
}
@@ -924,6 +936,11 @@ namespace MediaBrowser.Controller.Entities
}
}
+ if (query.ExcludeItemIds.Contains(item.Id))
+ {
+ return false;
+ }
+
return true;
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 04f47b729..1043029c6 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -152,16 +152,7 @@ namespace MediaBrowser.Controller.Entities
{
get
{
- if (!string.IsNullOrEmpty(PrimaryVersionId))
- {
- var item = LibraryManager.GetItemById(PrimaryVersionId);
- if (item is Video video)
- {
- return video.MediaSourceCount;
- }
- }
-
- return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
+ return GetMediaSourceCount();
}
}
@@ -259,6 +250,27 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override MediaType MediaType => MediaType.Video;
+ private int GetMediaSourceCount(HashSet<Guid> callstack = null)
+ {
+ callstack ??= new();
+ if (!string.IsNullOrEmpty(PrimaryVersionId))
+ {
+ var item = LibraryManager.GetItemById(PrimaryVersionId);
+ if (item is Video video)
+ {
+ if (callstack.Contains(video.Id))
+ {
+ return video.LinkedAlternateVersions.Length + video.LocalAlternateVersions.Length + 1;
+ }
+
+ callstack.Add(video.Id);
+ return video.GetMediaSourceCount(callstack);
+ }
+ }
+
+ return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
+ }
+
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
index 2742f21e3..b53210b0b 100644
--- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
@@ -167,12 +167,12 @@ public static class XmlReaderExtensions
// Only split by comma if there is no pipe in the string
// We have to be careful to not split names like Matthew, Jr.
- var separator = !value.Contains('|', StringComparison.Ordinal)
+ ReadOnlySpan<char> separator = !value.Contains('|', StringComparison.Ordinal)
&& !value.Contains(';', StringComparison.Ordinal)
- ? new[] { ',' }
- : new[] { '|', ';' };
+ ? stackalloc[] { ',' }
+ : stackalloc[] { '|', ';' };
- foreach (var part in value.Trim().Trim(separator).Split(separator))
+ foreach (var part in value.AsSpan().Trim().Trim(separator).ToString().Split(separator))
{
if (!string.IsNullOrWhiteSpace(part))
{
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
index a97096eae..7e235ed26 100644
--- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -60,8 +60,15 @@ namespace MediaBrowser.Controller
void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences);
/// <summary>
- /// Saves changes made to the database.
+ /// Updates or Creates the display preferences.
/// </summary>
- void SaveChanges();
+ /// <param name="displayPreferences">The entity to update or create.</param>
+ void UpdateDisplayPreferences(DisplayPreferences displayPreferences);
+
+ /// <summary>
+ /// Updates or Creates the display preferences for the given item.
+ /// </summary>
+ /// <param name="itemDisplayPreferences">The entity to update or create.</param>
+ void UpdateItemDisplayPreferences(ItemDisplayPreferences itemDisplayPreferences);
}
}
diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs
index 1a33c3aa8..3e390ca42 100644
--- a/MediaBrowser.Controller/IO/FileSystemHelper.cs
+++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using MediaBrowser.Model.IO;
@@ -61,4 +62,82 @@ public static class FileSystemHelper
}
}
}
+
+ /// <summary>
+ /// Gets the target of the specified file link.
+ /// </summary>
+ /// <remarks>
+ /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
+ /// </remarks>
+ /// <param name="linkPath">The path of the file link.</param>
+ /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
+ /// <returns>
+ /// A <see cref="FileInfo"/> if the <paramref name="linkPath"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
+ /// </returns>
+ public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
+ {
+ // Check if the file exists so the native resolve handler won't throw at us.
+ if (!File.Exists(linkPath))
+ {
+ return null;
+ }
+
+ if (!returnFinalTarget)
+ {
+ return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo;
+ }
+
+ if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo)
+ {
+ return null;
+ }
+
+ if (!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)
+ {
+ var targetPath = linkInfo.FullName;
+
+ // If an infinite loop is detected, return the file info for the
+ // first link in the loop we encountered.
+ if (!visited.Add(targetPath))
+ {
+ return new FileInfo(targetPath);
+ }
+
+ targetInfo = linkInfo;
+ currentPath = targetPath;
+
+ // Exit if the target doesn't exist, so the native resolve handler won't throw at us.
+ if (!targetInfo.Exists)
+ {
+ break;
+ }
+ }
+
+ return targetInfo;
+ }
+
+ /// <summary>
+ /// Gets the target of the specified file link.
+ /// </summary>
+ /// <remarks>
+ /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
+ /// </remarks>
+ /// <param name="fileInfo">The file info of the file link.</param>
+ /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param>
+ /// <returns>
+ /// A <see cref="FileInfo"/> if the <paramref name="fileInfo"/> is a link, regardless of if the target exists; otherwise, <c>null</c>.
+ /// </returns>
+ public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false)
+ {
+ ArgumentNullException.ThrowIfNull(fileInfo);
+
+ return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget);
+ }
}
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 7c20164a6..eb6743754 100644
--- a/MediaBrowser.Controller/IO/IPathManager.cs
+++ b/MediaBrowser.Controller/IO/IPathManager.cs
@@ -1,10 +1,10 @@
+using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.IO;
/// <summary>
-/// Interface ITrickplayManager.
+/// Interface IPathManager.
/// </summary>
public interface IPathManager
{
@@ -46,4 +46,26 @@ public interface IPathManager
/// <param name="mediaSourceId">The media source id.</param>
/// <returns>The absolute path.</returns>
public string GetAttachmentFolderPath(string mediaSourceId);
+
+ /// <summary>
+ /// Gets the chapter images data path.
+ /// </summary>
+ /// <param name="item">The base item.</param>
+ /// <returns>The chapter images data path.</returns>
+ public string GetChapterImageFolderPath(BaseItem item);
+
+ /// <summary>
+ /// Gets the chapter images path.
+ /// </summary>
+ /// <param name="item">The base item.</param>
+ /// <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/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs
index 608286cd8..a6e83a02c 100644
--- a/MediaBrowser.Controller/IServerApplicationPaths.cs
+++ b/MediaBrowser.Controller/IServerApplicationPaths.cs
@@ -2,6 +2,10 @@
#pragma warning disable CS1591
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using MediaBrowser.Common.Configuration;
namespace MediaBrowser.Controller
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..fcc5ed672 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.
@@ -337,6 +337,13 @@ namespace MediaBrowser.Controller.Library
void DeleteItem(BaseItem item, DeleteOptions options);
/// <summary>
+ /// Deletes items that are not having any children like Actors.
+ /// </summary>
+ /// <param name="items">Items to delete.</param>
+ /// <remarks>In comparison to <see cref="DeleteItem(BaseItem, DeleteOptions, BaseItem, bool)"/> this method skips a lot of steps assuming there are no children to recusively delete nor does it define the special handling for channels and alike.</remarks>
+ public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items);
+
+ /// <summary>
/// Deletes the item.
/// </summary>
/// <param name="item">Item to delete.</param>
@@ -593,11 +600,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);
@@ -624,12 +631,16 @@ namespace MediaBrowser.Controller.Library
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query);
+ IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names);
+
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query);
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query);
int GetCount(InternalItemsQuery query);
+ ItemCounts GetItemCounts(InternalItemsQuery query);
+
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
BaseItem GetParentItem(Guid? parentId, Guid? userId);
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/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 3353ad63f..b5d14e94b 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
- <VersionPrefix>10.11.0</VersionPrefix>
+ <VersionPrefix>10.12.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 7c3138002..a1d891535 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 HDR10+ or a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
+ if (videoStream.VideoRangeType is VideoRangeType.HDR10Plus or 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)
@@ -6476,7 +6552,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (isD3d11Supported && isCodecAvailable)
{
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11 -noautorotate" + stripRotationDataArgs : string.Empty)
- + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty);
}
}
@@ -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/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 8d6211051..43680f5c0 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Enums;
@@ -22,8 +21,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// For now, a common base class until the API and MediaEncoding classes are unified
public class EncodingJobInfo
{
- public int? OutputAudioBitrate;
- public int? OutputAudioChannels;
+ private static readonly char[] _separators = ['|', ','];
private TranscodeReason? _transcodeReasons = null;
@@ -36,6 +34,10 @@ namespace MediaBrowser.Controller.MediaEncoding
SupportedSubtitleCodecs = Array.Empty<string>();
}
+ public int? OutputAudioBitrate { get; set; }
+
+ public int? OutputAudioChannels { get; set; }
+
public TranscodeReason TranscodeReasons
{
get
@@ -586,7 +588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (!string.IsNullOrEmpty(BaseRequest.Profile))
{
- return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ return BaseRequest.Profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
if (!string.IsNullOrEmpty(codec))
@@ -595,7 +597,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(profile))
{
- return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ return profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
}
@@ -606,7 +608,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
{
- return BaseRequest.VideoRangeType.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ return BaseRequest.VideoRangeType.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
if (!string.IsNullOrEmpty(codec))
@@ -615,7 +617,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(rangetype))
{
- return rangetype.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ return rangetype.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
}
@@ -626,7 +628,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (!string.IsNullOrEmpty(BaseRequest.CodecTag))
{
- return BaseRequest.CodecTag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ return BaseRequest.CodecTag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
if (!string.IsNullOrEmpty(codec))
@@ -635,7 +637,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codectag))
{
- return codectag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ return codectag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
}
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/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
deleted file mode 100644
index 8ce40a58d..000000000
--- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.MediaEncoding
-{
- public interface IEncodingManager
- {
- /// <summary>
- /// Refreshes the chapter images.
- /// </summary>
- /// <param name="video">Video to use.</param>
- /// <param name="directoryService">Directory service to use.</param>
- /// <param name="chapters">Set of chapters to refresh.</param>
- /// <param name="extractImages">Option to extract images.</param>
- /// <param name="saveChapters">Option to save chapters.</param>
- /// <param name="cancellationToken">CancellationToken to use for operation.</param>
- /// <returns><c>true</c> if successful, <c>false</c> if not.</returns>
- Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
index 9bf27b3b2..bdd75da2f 100644
--- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
@@ -53,5 +53,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>System.String.</returns>
Task<string> GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Extracts all extractable subtitles (text and pgs).
+ /// </summary>
+ /// <param name="mediaSource">The mediaSource.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken);
}
}
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/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 4757bfa30..1e0d77fe5 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -81,6 +81,16 @@ namespace MediaBrowser.Controller.Net
protected abstract Task<TReturnDataType> GetDataToSend();
/// <summary>
+ /// Gets the data to send for a specific connection.
+ /// </summary>
+ /// <param name="connection">The connection.</param>
+ /// <returns>Task{`1}.</returns>
+ protected virtual Task<TReturnDataType> GetDataToSendForConnection(IWebSocketConnection connection)
+ {
+ return GetDataToSend();
+ }
+
+ /// <summary>
/// Processes the message.
/// </summary>
/// <param name="message">The message.</param>
@@ -174,17 +184,11 @@ namespace MediaBrowser.Controller.Net
continue;
}
- var data = await GetDataToSend().ConfigureAwait(false);
- if (data is null)
- {
- continue;
- }
-
IEnumerable<Task> GetTasks()
{
foreach (var tuple in tuples)
{
- yield return SendDataInternal(data, tuple);
+ yield return SendDataForConnectionAsync(tuple);
}
}
@@ -198,12 +202,19 @@ namespace MediaBrowser.Controller.Net
}
}
- private async Task SendDataInternal(TReturnDataType data, (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple)
+ private async Task SendDataForConnectionAsync((IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple)
{
try
{
var (connection, cts, state) = tuple;
var cancellationToken = cts.Token;
+
+ var data = await GetDataToSendForConnection(connection).ConfigureAwait(false);
+ if (data is null)
+ {
+ return;
+ }
+
await connection.SendAsync(
new OutboundWebSocketMessage<TReturnDataType> { MessageType = Type, Data = data },
cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Controller/Chapters/IChapterRepository.cs b/MediaBrowser.Controller/Persistence/IChapterRepository.cs
index e22cb0f58..64b90fd63 100644
--- a/MediaBrowser.Controller/Chapters/IChapterRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IChapterRepository.cs
@@ -1,36 +1,30 @@
using System;
using System.Collections.Generic;
-using MediaBrowser.Model.Dto;
+using System.Threading;
+using System.Threading.Tasks;
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Controller.Chapters;
+namespace MediaBrowser.Controller.Persistence;
/// <summary>
-/// Interface IChapterManager.
+/// Interface IChapterRepository.
/// </summary>
public interface IChapterRepository
{
/// <summary>
- /// Saves the chapters.
+ /// Deletes the chapters.
/// </summary>
/// <param name="itemId">The item.</param>
- /// <param name="chapters">The set of chapters.</param>
- void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
-
- /// <summary>
- /// Gets all chapters associated with the baseItem.
- /// </summary>
- /// <param name="baseItem">The baseitem.</param>
- /// <returns>A readonly list of chapter instances.</returns>
- IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem);
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task DeleteChaptersAsync(Guid itemId, CancellationToken cancellationToken);
/// <summary>
- /// Gets a single chapter of a BaseItem on a specific index.
+ /// Saves the chapters.
/// </summary>
- /// <param name="baseItem">The baseitem.</param>
- /// <param name="index">The index of that chapter.</param>
- /// <returns>A chapter instance.</returns>
- ChapterInfo? GetChapter(BaseItemDto baseItem, int index);
+ /// <param name="itemId">The item.</param>
+ /// <param name="chapters">The set of chapters.</param>
+ void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
/// <summary>
/// Gets all chapters associated with the baseItem.
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index e185898bf..0026ab2b5 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -5,8 +5,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
+using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
@@ -20,8 +23,8 @@ public interface IItemRepository
/// <summary>
/// Deletes the item.
/// </summary>
- /// <param name="id">The identifier.</param>
- void DeleteItem(Guid id);
+ /// <param name="ids">The identifier to delete.</param>
+ void DeleteItem(params IReadOnlyList<Guid> ids);
/// <summary>
/// Saves the items.
@@ -83,6 +86,8 @@ public interface IItemRepository
int GetCount(InternalItemsQuery filter);
+ ItemCounts GetItemCounts(InternalItemsQuery filter);
+
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter);
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter);
@@ -102,4 +107,27 @@ 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);
+
+ /// <summary>
+ /// Gets a value indicating wherever all children of the requested Id has been played.
+ /// </summary>
+ /// <param name="user">The userdata to check against.</param>
+ /// <param name="id">The Top id to check.</param>
+ /// <param name="recursive">Whever the check should be done recursive. Warning expensive operation.</param>
+ /// <returns>A value indicating whever all children has been played.</returns>
+ bool GetIsPlayed(User user, Guid id, bool recursive);
+
+ /// <summary>
+ /// Gets all artist matches from the db.
+ /// </summary>
+ /// <param name="artistNames">The names of the artists.</param>
+ /// <returns>A map of the artist name and the potential matches.</returns>
+ IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames);
}
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/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 1062399e3..fc367b829 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -149,9 +149,11 @@ namespace MediaBrowser.Controller.Playlists
return [];
}
- public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{
- return GetPlayableItems(user, query);
+ var items = GetPlayableItems(user, query);
+ totalCount = items.Count;
+ return items;
}
public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetManageableItems()
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.