mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-04-02 16:32:07 +01:00
Merge branch 'master' into issue15137
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -1127,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),
|
||||
@@ -1134,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,
|
||||
@@ -1610,12 +1620,17 @@ namespace MediaBrowser.Controller.Entities
|
||||
return isAllowed;
|
||||
}
|
||||
|
||||
if (maxAllowedSubRating is not null)
|
||||
if (!maxAllowedRating.HasValue)
|
||||
{
|
||||
return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value;
|
||||
if (ratingScore.Score != maxAllowedRating.Value)
|
||||
{
|
||||
return ratingScore.Score < maxAllowedRating.Value;
|
||||
}
|
||||
|
||||
return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
|
||||
}
|
||||
|
||||
public ParentalRatingScore GetParentalRatingScore()
|
||||
@@ -2038,6 +2053,9 @@ namespace MediaBrowser.Controller.Entities
|
||||
public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
=> await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
|
||||
await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that images within the item are still on the filesystem.
|
||||
/// </summary>
|
||||
|
||||
@@ -452,6 +452,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
// That's all the new and changed ones - now see if any have been removed and need cleanup
|
||||
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
|
||||
var shouldRemove = !IsRoot || allowRemoveRoot;
|
||||
var actuallyRemoved = new List<BaseItem>();
|
||||
// If it's an AggregateFolder, don't remove
|
||||
if (shouldRemove && itemsRemoved.Count > 0)
|
||||
{
|
||||
@@ -467,6 +468,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
Logger.LogDebug("Removed item: {Path}", item.Path);
|
||||
|
||||
actuallyRemoved.Add(item);
|
||||
item.SetParent(null);
|
||||
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
|
||||
}
|
||||
@@ -477,6 +479,20 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
LibraryManager.CreateItems(newItems, this, cancellationToken);
|
||||
}
|
||||
|
||||
// After removing items, reattach any detached user data to remaining children
|
||||
// that share the same user data keys (eg. same episode replaced with a new file).
|
||||
if (actuallyRemoved.Count > 0)
|
||||
{
|
||||
var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet();
|
||||
foreach (var child in validChildren)
|
||||
{
|
||||
if (child.GetUserDataKeys().Any(removedKeys.Contains))
|
||||
{
|
||||
await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -715,14 +731,21 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
else
|
||||
{
|
||||
items = GetRecursiveChildren(user, query, out totalCount);
|
||||
// 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; // override these here as they have already been applied
|
||||
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;
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
result.TotalRecordCount = totalCount;
|
||||
return result;
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
if (this is not UserRootFolder
|
||||
@@ -980,25 +1003,19 @@ namespace MediaBrowser.Controller.Entities
|
||||
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,
|
||||
Limit = query.Limit,
|
||||
StartIndex = query.StartIndex,
|
||||
NameStartsWith = query.NameStartsWith,
|
||||
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
|
||||
NameLessThan = query.NameLessThan
|
||||
};
|
||||
|
||||
items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
|
||||
|
||||
query.Limit = null;
|
||||
query.StartIndex = null;
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
result.TotalRecordCount = totalItemCount;
|
||||
return result;
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
|
||||
@@ -1034,7 +1051,15 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
|
||||
}
|
||||
|
||||
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
|
||||
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(
|
||||
@@ -1047,12 +1072,49 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
||||
if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
|
||||
if (!CollapseBoxSetItems(query, queryParent, user, configurationManager))
|
||||
{
|
||||
items = collectionManager.CollapseItemsWithinBoxSets(items, user);
|
||||
return items;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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(
|
||||
@@ -1083,24 +1145,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;
|
||||
}
|
||||
|
||||
if (query.IncludeItemTypes.Length == 0
|
||||
|| query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series))
|
||||
{
|
||||
param = true;
|
||||
}
|
||||
return param.Value && AllowBoxSetCollapsing(query);
|
||||
}
|
||||
|
||||
return param.HasValue && param.Value && AllowBoxSetCollapsing(query);
|
||||
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 (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query);
|
||||
}
|
||||
|
||||
private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
|
||||
@@ -1358,13 +1422,6 @@ namespace MediaBrowser.Controller.Entities
|
||||
.Where(e => query is null || UserViewBuilder.FilterItem(e, query))
|
||||
.ToArray();
|
||||
|
||||
if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0))
|
||||
{
|
||||
realChildren = realChildren
|
||||
.OrderBy(e => e.ProductionYear ?? int.MaxValue)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var childCount = realChildren.Length;
|
||||
if (result.Count < limit)
|
||||
{
|
||||
|
||||
@@ -125,6 +125,8 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public bool? UseRawName { get; set; }
|
||||
|
||||
public string? Person { get; set; }
|
||||
|
||||
public Guid[] PersonIds { get; set; }
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||
|
||||
if (sortBy == ItemSortBy.Default)
|
||||
{
|
||||
return items;
|
||||
return items;
|
||||
}
|
||||
|
||||
return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending);
|
||||
@@ -136,6 +136,12 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||
return Sort(children, user).ToArray();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null)
|
||||
{
|
||||
var children = base.GetChildren(user, includeLinkedChildren, out totalItemCount, query);
|
||||
return Sort(children, user).ToArray();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
|
||||
{
|
||||
var children = base.GetRecursiveChildren(user, query, out totalCount);
|
||||
|
||||
@@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
query.AncestorWithPresentationUniqueKey = null;
|
||||
query.SeriesPresentationUniqueKey = seriesKey;
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.Season };
|
||||
query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) };
|
||||
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
|
||||
if (user is not null && !user.DisplayMissingEpisodes)
|
||||
{
|
||||
@@ -247,6 +247,10 @@ 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)
|
||||
{
|
||||
|
||||
@@ -455,7 +455,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
|
||||
var totalCount = itemsArray.Length;
|
||||
|
||||
if (query.Limit.HasValue)
|
||||
if (query.Limit.HasValue && query.Limit.Value > 0)
|
||||
{
|
||||
itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -61,4 +62,108 @@ public static class FileSystemHelper
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a single link hop for the specified path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns <c>null</c> if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT).
|
||||
/// </remarks>
|
||||
/// <param name="path">The file path to resolve.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="FileInfo"/> representing the next link target if the path is a link; otherwise, <c>null</c>.
|
||||
/// </returns>
|
||||
private static FileInfo? Resolve(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Filesystem doesn't support links (e.g., exFAT).
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target of the specified file link.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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 Resolve(linkPath);
|
||||
}
|
||||
|
||||
var targetInfo = Resolve(linkPath);
|
||||
if (targetInfo is null || !targetInfo.Exists)
|
||||
{
|
||||
return targetInfo;
|
||||
}
|
||||
|
||||
var currentPath = targetInfo.FullName;
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath };
|
||||
|
||||
while (true)
|
||||
{
|
||||
var linkInfo = Resolve(currentPath);
|
||||
if (linkInfo is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var targetPath = linkInfo.FullName;
|
||||
|
||||
// If an infinite loop is detected, return the file info for the
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +281,14 @@ namespace MediaBrowser.Controller.Library
|
||||
/// <returns>Returns a Task that can be awaited.</returns>
|
||||
Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reattaches the user data to the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous reattachment operation.</returns>
|
||||
Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the item.
|
||||
/// </summary>
|
||||
@@ -652,5 +660,12 @@ namespace MediaBrowser.Controller.Library
|
||||
/// This exists so plugins can trigger a library scan.
|
||||
/// </remarks>
|
||||
void QueueLibraryScan();
|
||||
|
||||
/// <summary>
|
||||
/// Add mblink file for a media path.
|
||||
/// </summary>
|
||||
/// <param name="virtualFolderPath">The path to the virtualfolder.</param>
|
||||
/// <param name="pathInfo">The new virtualfolder.</param>
|
||||
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -29,7 +30,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
/// </summary>
|
||||
private readonly Lock _taskLock = new();
|
||||
|
||||
private readonly BlockingCollection<TaskQueueItem> _tasks = new();
|
||||
private readonly Channel<TaskQueueItem> _tasks = Channel.CreateUnbounded<TaskQueueItem>();
|
||||
|
||||
private volatile int _workCounter;
|
||||
private Task? _cleanupTask;
|
||||
@@ -77,7 +78,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
|
||||
lock (_taskLock)
|
||||
{
|
||||
if (_tasks.Count > 0 || _workCounter > 0)
|
||||
if (_tasks.Reader.Count > 0 || _workCounter > 0)
|
||||
{
|
||||
_logger.LogDebug("Delay cleanup task, operations still running.");
|
||||
// tasks are still there so its still in use. Reschedule cleanup task.
|
||||
@@ -144,9 +145,9 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
_deadlockDetector.Value = stopToken.TaskStop;
|
||||
try
|
||||
{
|
||||
foreach (var item in _tasks.GetConsumingEnumerable(stopToken.GlobalStop.Token))
|
||||
while (!stopToken.GlobalStop.Token.IsCancellationRequested)
|
||||
{
|
||||
stopToken.GlobalStop.Token.ThrowIfCancellationRequested();
|
||||
var item = await _tasks.Reader.ReadAsync(stopToken.GlobalStop.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0;
|
||||
@@ -187,7 +188,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
|
||||
await item.Worker(item.Data).ConfigureAwait(true);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while performing a library operation");
|
||||
}
|
||||
@@ -242,7 +243,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
if (ShouldForceSequentialOperation())
|
||||
if (ShouldForceSequentialOperation() || _deadlockDetector.Value is not null)
|
||||
{
|
||||
_logger.LogDebug("Process sequentially.");
|
||||
try
|
||||
@@ -264,35 +265,14 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
for (var i = 0; i < workItems.Length; i++)
|
||||
{
|
||||
var item = workItems[i]!;
|
||||
_tasks.Add(item, CancellationToken.None);
|
||||
await _tasks.Writer.WriteAsync(item, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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/>
|
||||
@@ -304,13 +284,12 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_tasks.CompleteAdding();
|
||||
_tasks.Writer.Complete();
|
||||
foreach (var item in _taskRunners)
|
||||
{
|
||||
await item.Key.CancelAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_tasks.Dispose();
|
||||
if (_cleanupTask is not null)
|
||||
{
|
||||
await _cleanupTask.ConfigureAwait(false);
|
||||
|
||||
@@ -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>
|
||||
@@ -19,9 +19,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BitFaster.Caching" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="System.Threading.Tasks.Dataflow" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -36,7 +34,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
|
||||
@@ -43,8 +43,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public bool AllowAudioStreamCopy { get; set; }
|
||||
|
||||
public bool BreakOnNonKeyFrames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the audio sample rate.
|
||||
/// </summary>
|
||||
|
||||
@@ -2378,6 +2378,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// If SDR is the only supported range, we should not copy any of the HDR streams.
|
||||
// All the following copy check assumes at least one HDR format is supported.
|
||||
if (requestedRangeTypes.Length == 1 && requestHasSDR && videoStream.VideoRangeType != VideoRangeType.SDR)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the client does not support DOVI and the video stream is DOVI without fallback, we should not copy it.
|
||||
if (!requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVI)
|
||||
{
|
||||
@@ -2390,8 +2397,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|
||||
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
|
||||
{
|
||||
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
|
||||
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG)
|
||||
// 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;
|
||||
}
|
||||
@@ -2907,8 +2914,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (time > 0)
|
||||
{
|
||||
// For direct streaming/remuxing, we seek at the exact position of the keyframe
|
||||
// However, ffmpeg will seek to previous keyframe when the exact time is the input
|
||||
// For direct streaming/remuxing, HLS segments start at keyframes.
|
||||
// However, ffmpeg will seek to previous keyframe when the exact frame time is the input
|
||||
// Workaround this by adding 0.5s offset to the seeking time to get the exact keyframe on most videos.
|
||||
// This will help subtitle syncing.
|
||||
var isHlsRemuxing = state.IsVideoRequest && state.TranscodingType is TranscodingJobType.Hls && IsCopyCodec(state.OutputVideoCodec);
|
||||
@@ -2925,17 +2932,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (state.IsVideoRequest)
|
||||
{
|
||||
var outputVideoCodec = GetVideoEncoder(state, options);
|
||||
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
|
||||
|
||||
// Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking
|
||||
// Disable -noaccurate_seek on mpegts container due to the timestamps issue on some clients,
|
||||
// but it's still required for fMP4 container otherwise the audio can't be synced to the video.
|
||||
if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)
|
||||
&& state.TranscodingType != TranscodingJobType.Progressive
|
||||
&& !state.EnableBreakOnNonKeyFrames(outputVideoCodec)
|
||||
&& (state.BaseRequest.StartTimeTicks ?? 0) > 0)
|
||||
// If we are remuxing, then the copied stream cannot be seeked accurately (it will seek to the nearest
|
||||
// keyframe). If we are using fMP4, then force all other streams to use the same inaccurate seeking to
|
||||
// avoid A/V sync issues which cause playback issues on some devices.
|
||||
// When remuxing video, the segment start times correspond to key frames in the source stream, so this
|
||||
// option shouldn't change the seeked point that much.
|
||||
// Important: make sure not to use it with wtv because it breaks seeking
|
||||
if (state.TranscodingType is TranscodingJobType.Hls
|
||||
&& string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)
|
||||
&& (IsCopyCodec(state.OutputVideoCodec) || IsCopyCodec(state.OutputAudioCodec))
|
||||
&& !string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
seekParam += " -noaccurate_seek";
|
||||
}
|
||||
@@ -5942,28 +5948,37 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap;
|
||||
var swapOutputWandH = doRkVppTranspose && swapWAndH;
|
||||
var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts
|
||||
var outFormat = doOclTonemap ? "p010" : "nv12";
|
||||
var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var doScaling = !string.IsNullOrEmpty(GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH));
|
||||
|
||||
if (!hasSubs
|
||||
|| doRkVppTranspose
|
||||
|| !isFullAfbcPipeline
|
||||
|| !string.IsNullOrEmpty(doScaling))
|
||||
|| doScaling)
|
||||
{
|
||||
var isScaleRatioSupported = IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f);
|
||||
|
||||
// RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation,
|
||||
// but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it
|
||||
if (!string.IsNullOrEmpty(doScaling)
|
||||
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
|
||||
if (doScaling && !isScaleRatioSupported)
|
||||
{
|
||||
// Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
|
||||
// Use NV15 instead of P010 to avoid the issue.
|
||||
// SDR inputs are using BGRA formats already which is not affected.
|
||||
var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
|
||||
var intermediateFormat = doOclTonemap ? "nv15" : (isMjpegEncoder ? "bgra" : outFormat);
|
||||
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1";
|
||||
mainFilters.Add(hwScaleFilterFirstPass);
|
||||
}
|
||||
|
||||
// The RKMPP MJPEG encoder on some newer chip models no longer supports RGB input.
|
||||
// Use 2pass here to enable RGA output of full-range YUV in the 2nd pass.
|
||||
if (isMjpegEncoder && !doOclTonemap && ((doScaling && isScaleRatioSupported) || !doScaling))
|
||||
{
|
||||
var hwScaleFilterFirstPass = "vpp_rkrga=format=bgra:afbc=1";
|
||||
mainFilters.Add(hwScaleFilterFirstPass);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose)
|
||||
{
|
||||
hwScaleFilter += $":transpose={transposeDir}";
|
||||
@@ -6343,6 +6358,21 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
// Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format
|
||||
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase)
|
||||
|| videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
|
||||
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
|
||||
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var decoder = hardwareAccelerationType switch
|
||||
{
|
||||
HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth),
|
||||
@@ -7023,8 +7053,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var accelType = GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
|
||||
return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
|
||||
// there's an issue about AV1 AFBC on RK3588, disable it for now until it's fixed upstream
|
||||
return GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7053,7 +7083,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
public void TryStreamCopy(EncodingJobInfo state)
|
||||
public void TryStreamCopy(EncodingJobInfo state, EncodingOptions options)
|
||||
{
|
||||
if (state.VideoStream is not null && CanStreamCopyVideo(state, state.VideoStream))
|
||||
{
|
||||
@@ -7070,8 +7100,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
var preventHlsAudioCopy = state.TranscodingType is TranscodingJobType.Hls
|
||||
&& state.VideoStream is not null
|
||||
&& !IsCopyCodec(state.OutputVideoCodec)
|
||||
&& options.HlsAudioSeekStrategy is HlsAudioSeekStrategy.TranscodeAudio;
|
||||
|
||||
if (state.AudioStream is not null
|
||||
&& CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs))
|
||||
&& CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)
|
||||
&& !preventHlsAudioCopy)
|
||||
{
|
||||
state.OutputAudioCodec = "copy";
|
||||
}
|
||||
|
||||
@@ -515,21 +515,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public int HlsListSize => 0;
|
||||
|
||||
public bool EnableBreakOnNonKeyFrames(string videoCodec)
|
||||
{
|
||||
if (TranscodingType != TranscodingJobType.Progressive)
|
||||
{
|
||||
if (IsSegmentedLiveStream)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int? GetMediaStreamCount(MediaStreamType type, int limit)
|
||||
{
|
||||
var count = MediaSource.GetStreamCount(type);
|
||||
|
||||
@@ -27,10 +27,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
using (target)
|
||||
using (reader)
|
||||
{
|
||||
while (!reader.EndOfStream && reader.BaseStream.CanRead)
|
||||
string line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
while (line is not null && reader.BaseStream.CanRead)
|
||||
{
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
ParseLogLine(line, state);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
|
||||
@@ -50,6 +49,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
await target.FlushAsync().ConfigureAwait(false);
|
||||
line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,15 @@ public interface IItemRepository
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken);
|
||||
|
||||
void SaveImages(BaseItem item);
|
||||
Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reattaches the user data to the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous reattachment operation.</returns>
|
||||
Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the item.
|
||||
|
||||
@@ -350,5 +350,12 @@ namespace MediaBrowser.Controller.Session
|
||||
/// <param name="sessionIdOrPlaySessionId">The session id or playsession id.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dto for session info.
|
||||
/// </summary>
|
||||
/// <param name="sessionInfo">The session info.</param>
|
||||
/// <returns><see cref="SessionInfoDto"/> of the session.</returns>
|
||||
SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
@@ -9,7 +11,7 @@ namespace MediaBrowser.Controller.Sorting
|
||||
{
|
||||
public static class SortExtensions
|
||||
{
|
||||
private static readonly AlphanumericComparator _comparer = new AlphanumericComparator();
|
||||
private static readonly StringComparer _comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
|
||||
|
||||
public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user