Merge branch 'master' into master

This commit is contained in:
Joshua M. Boniface
2025-08-03 17:27:17 -04:00
committed by GitHub
956 changed files with 43950 additions and 11532 deletions

View File

@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Authentication

View File

@@ -4,7 +4,7 @@
using System;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Authentication

View File

@@ -7,8 +7,9 @@ using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Querying;

View File

@@ -0,0 +1,55 @@
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 images.
/// </summary>
/// <param name="video">Video to use.</param>
void DeleteChapterImages(Video video);
}

View File

@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;

View File

@@ -1,10 +1,10 @@
using System;
using System.Threading.Tasks;
using Jellyfin.Data.Dtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;

View File

@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;

View File

@@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Dto
EnableUserData = true;
AddCurrentProgram = true;
Fields = allFields ? AllItemFields : Array.Empty<ItemFields>();
Fields = allFields ? AllItemFields : [];
ImageTypes = AllImageTypes;
}

View File

@@ -1,7 +1,7 @@
#pragma warning disable CA1002
using System.Collections.Generic;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;

View File

@@ -8,8 +8,10 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;

View File

@@ -8,8 +8,10 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -138,11 +140,9 @@ namespace MediaBrowser.Controller.Entities.Audio
private static List<string> GetUserDataKeys(MusicArtist item)
{
var list = new List<string>();
var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist);
if (!string.IsNullOrEmpty(id))
if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId))
{
list.Add("Artist-Musicbrainz-" + id);
list.Add("Artist-Musicbrainz-" + externalId);
}
list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics());

View File

@@ -12,8 +12,10 @@ using System.Text;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
@@ -21,7 +23,9 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
@@ -31,7 +35,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
@@ -49,7 +52,7 @@ namespace MediaBrowser.Controller.Entities
/// The supported image extensions.
/// </summary>
public static readonly string[] SupportedImageExtensions
= new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg" };
= [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"];
private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
{
@@ -445,7 +448,7 @@ namespace MediaBrowser.Controller.Entities
return Array.Empty<string>();
}
return new[] { Path };
return [Path];
}
}
@@ -481,7 +484,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; }
@@ -578,6 +581,9 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public int? InheritedParentalRatingValue { get; set; }
[JsonIgnore]
public int? InheritedParentalRatingSubValue { get; set; }
/// <summary>
/// Gets or sets the critic rating.
/// </summary>
@@ -919,7 +925,7 @@ namespace MediaBrowser.Controller.Entities
// Remove from middle if surrounded by spaces
sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
// Remove from end if followed by a space
// Remove from end if preceeded by a space
if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
{
sortable = sortable.Remove(sortable.Length - (search.Length + 1));
@@ -1260,7 +1266,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
/// Overrides the base implementation to refresh metadata for local trailers.
/// The base implementation to refresh metadata.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
@@ -1357,9 +1363,7 @@ namespace MediaBrowser.Controller.Entities
protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
{
var path = ContainingFolderPath;
return directoryService.GetFileSystemEntries(path);
return directoryService.GetFileSystemEntries(ContainingFolderPath);
}
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
@@ -1388,6 +1392,23 @@ namespace MediaBrowser.Controller.Entities
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
});
// Cleanup removed extras
var removedExtraIds = item.ExtraIds.Where(e => !newExtraIds.Contains(e)).ToArray();
if (removedExtraIds.Length > 0)
{
var removedExtras = LibraryManager.GetItemList(new InternalItemsQuery()
{
ItemIds = removedExtraIds
});
foreach (var removedExtra in removedExtras)
{
LibraryManager.DeleteItem(removedExtra, new DeleteOptions()
{
DeleteFileLocation = false
});
}
}
await Task.WhenAll(tasks).ConfigureAwait(false);
item.ExtraIds = newExtraIds;
@@ -1402,7 +1423,14 @@ namespace MediaBrowser.Controller.Entities
public virtual bool RequiresRefresh()
{
return false;
if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
{
return false;
}
var info = FileSystem.GetFileSystemInfo(Path);
return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
}
public virtual List<string> GetUserDataKeys()
@@ -1537,7 +1565,8 @@ namespace MediaBrowser.Controller.Entities
return false;
}
var maxAllowedRating = user.MaxParentalAgeRating;
var maxAllowedRating = user.MaxParentalRatingScore;
var maxAllowedSubRating = user.MaxParentalRatingSubScore;
var rating = CustomRatingForComparison;
if (string.IsNullOrEmpty(rating))
@@ -1551,10 +1580,10 @@ namespace MediaBrowser.Controller.Entities
return !GetBlockUnratedValue(user);
}
var value = LocalizationManager.GetRatingLevel(rating);
var ratingScore = LocalizationManager.GetRatingScore(rating);
// Could not determine rating level
if (!value.HasValue)
if (ratingScore is null)
{
var isAllowed = !GetBlockUnratedValue(user);
@@ -1566,10 +1595,15 @@ namespace MediaBrowser.Controller.Entities
return isAllowed;
}
return !maxAllowedRating.HasValue || value.Value <= maxAllowedRating.Value;
if (maxAllowedSubRating is not null)
{
return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value;
}
return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value;
}
public int? GetInheritedParentalRatingValue()
public ParentalRatingScore GetParentalRatingScore()
{
var rating = CustomRatingForComparison;
@@ -1583,7 +1617,7 @@ namespace MediaBrowser.Controller.Entities
return null;
}
return LocalizationManager.GetRatingLevel(rating);
return LocalizationManager.GetRatingScore(rating);
}
public List<string> GetInheritedTags()
@@ -1681,7 +1715,7 @@ namespace MediaBrowser.Controller.Entities
public virtual string GetClientTypeName()
{
if (IsFolder && SourceType == SourceType.Channel && this is not Channel)
if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not Series)
{
return "ChannelFolderItem";
}
@@ -1775,7 +1809,6 @@ namespace MediaBrowser.Controller.Entities
public void AddStudio(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
var current = Studios;
if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
@@ -1794,7 +1827,7 @@ namespace MediaBrowser.Controller.Entities
public void SetStudios(IEnumerable<string> names)
{
Studios = names.Distinct().ToArray();
Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
/// <summary>
@@ -1960,9 +1993,10 @@ namespace MediaBrowser.Controller.Entities
}
// Remove from file system
if (info.IsLocalFile)
var path = info.Path;
if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
{
FileSystem.DeleteFile(info.Path);
FileSystem.DeleteFile(path);
}
// Remove from item
@@ -1973,7 +2007,7 @@ namespace MediaBrowser.Controller.Entities
public void RemoveImage(ItemImageInfo image)
{
RemoveImages(new[] { image });
RemoveImages([image]);
}
public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
@@ -2008,7 +2042,7 @@ namespace MediaBrowser.Controller.Entities
continue;
}
(deletedImages ??= new List<ItemImageInfo>()).Add(imageInfo);
(deletedImages ??= []).Add(imageInfo);
}
var anyImagesRemoved = deletedImages?.Count > 0;
@@ -2040,7 +2074,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)
{
@@ -2090,7 +2124,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)
@@ -2211,11 +2245,7 @@ namespace MediaBrowser.Controller.Entities
{
return new[]
{
new FileSystemMetadata
{
FullName = Path,
IsDirectory = IsFolder
}
FileSystem.GetFileSystemInfo(Path)
}.Concat(GetLocalMetadataFilesToDelete());
}
@@ -2223,7 +2253,7 @@ namespace MediaBrowser.Controller.Entities
{
if (IsFolder || !IsInMixedFolder)
{
return new List<FileSystemMetadata>();
return [];
}
var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
@@ -2479,10 +2509,10 @@ namespace MediaBrowser.Controller.Entities
protected virtual List<string> GetEtagValues(User user)
{
return new List<string>
{
return
[
DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
};
];
}
public virtual IEnumerable<Guid> GetAncestorIds()
@@ -2502,7 +2532,7 @@ namespace MediaBrowser.Controller.Entities
public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
{
return new[] { Id };
return [Id];
}
public virtual double? GetRefreshProgress()
@@ -2516,11 +2546,29 @@ namespace MediaBrowser.Controller.Entities
var item = this;
var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? null;
if (inheritedParentalRatingValue != item.InheritedParentalRatingValue)
var rating = item.GetParentalRatingScore();
if (rating is not null)
{
item.InheritedParentalRatingValue = inheritedParentalRatingValue;
updateType |= ItemUpdateType.MetadataImport;
if (rating.Score != item.InheritedParentalRatingValue)
{
item.InheritedParentalRatingValue = rating.Score;
updateType |= ItemUpdateType.MetadataImport;
}
if (rating.SubScore != item.InheritedParentalRatingSubValue)
{
item.InheritedParentalRatingSubValue = rating.SubScore;
updateType |= ItemUpdateType.MetadataImport;
}
}
else
{
if (item.InheritedParentalRatingValue is not null)
{
item.InheritedParentalRatingValue = null;
item.InheritedParentalRatingSubValue = null;
updateType |= ItemUpdateType.MetadataImport;
}
}
return updateType;
@@ -2540,8 +2588,9 @@ namespace MediaBrowser.Controller.Entities
.Select(i => i.OfficialRating)
.Where(i => !string.IsNullOrEmpty(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(rating => (rating, LocalizationManager.GetRatingLevel(rating)))
.OrderBy(i => i.Item2 ?? 1000)
.Select(rating => (rating, LocalizationManager.GetRatingScore(rating)))
.OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
.ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
.Select(i => i.rating);
OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;

View File

@@ -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;
}
}
}

View File

@@ -11,8 +11,8 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;

View File

@@ -11,10 +11,11 @@ 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.Entities;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Collections;
@@ -23,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;
@@ -47,6 +49,8 @@ namespace MediaBrowser.Controller.Entities
public static IUserViewManager UserViewManager { get; set; }
public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is root.
/// </summary>
@@ -596,51 +600,13 @@ namespace MediaBrowser.Controller.Entities
/// <returns>Task.</returns>
private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progress, CancellationToken cancellationToken)
{
var childrenCount = children.Count;
var childrenProgress = new double[childrenCount];
void UpdateProgress()
{
progress.Report(childrenProgress.Average());
}
var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount;
var actionBlock = new ActionBlock<int>(
async i =>
{
var innerProgress = new Progress<double>(innerPercent =>
{
// round the percent and only update progress if it changed to prevent excessive UpdateProgress calls
var innerPercentRounded = Math.Round(innerPercent);
if (childrenProgress[i] != innerPercentRounded)
{
childrenProgress[i] = innerPercentRounded;
UpdateProgress();
}
});
await task(children[i], innerProgress).ConfigureAwait(false);
childrenProgress[i] = 100;
UpdateProgress();
},
new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = parallelism,
CancellationToken = cancellationToken,
});
for (var i = 0; i < childrenCount; i++)
{
await actionBlock.SendAsync(i, cancellationToken).ConfigureAwait(false);
}
actionBlock.Complete();
await actionBlock.Completion.ConfigureAwait(false);
await LimitedConcurrencyLibraryScheduler
.Enqueue(
children.ToArray(),
task,
progress,
cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
@@ -729,7 +695,7 @@ namespace MediaBrowser.Controller.Entities
items = GetRecursiveChildren(user, query);
}
return PostFilterAndSort(items, query, true);
return PostFilterAndSort(items, query);
}
if (this is not UserRootFolder
@@ -993,10 +959,10 @@ namespace MediaBrowser.Controller.Entities
items = GetChildren(user, true, childQuery).Where(filter);
}
return PostFilterAndSort(items, query, true);
return PostFilterAndSort(items, query);
}
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
{
var user = query.User;
@@ -1006,7 +972,7 @@ namespace MediaBrowser.Controller.Entities
items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
}
#pragma warning disable CA1309
#pragma warning disable CA1309
if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
{
items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1);
@@ -1021,7 +987,7 @@ namespace MediaBrowser.Controller.Entities
{
items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1);
}
#pragma warning restore CA1309
#pragma warning restore CA1309
// This must be the last filter
if (!query.AdjacentTo.IsNullOrEmpty())
@@ -1029,7 +995,7 @@ namespace MediaBrowser.Controller.Entities
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
}
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
}
private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
@@ -1062,11 +1028,6 @@ namespace MediaBrowser.Controller.Entities
return false;
}
if (queryParent is Series)
{
return false;
}
if (queryParent is Season)
{
return false;
@@ -1086,12 +1047,15 @@ namespace MediaBrowser.Controller.Entities
if (!param.HasValue)
{
if (user is not null && !configurationManager.Configuration.EnableGroupingIntoCollections)
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.Contains(BaseItemKind.Movie))
if (query.IncludeItemTypes.Length == 0
|| query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series))
{
param = true;
}
@@ -1222,11 +1186,6 @@ namespace MediaBrowser.Controller.Entities
return false;
}
if (request.IsPlayed.HasValue)
{
return false;
}
if (!string.IsNullOrWhiteSpace(request.Person))
{
return false;
@@ -1267,21 +1226,19 @@ namespace MediaBrowser.Controller.Entities
return false;
}
if (request.MinCommunityRating.HasValue)
{
return false;
}
if (request.MinCriticRating.HasValue)
{
return false;
}
if (request.MinIndexNumber.HasValue)
{
return false;
}
if (request.OrderBy.Any(o =>
o.OrderBy == ItemSortBy.CommunityRating ||
o.OrderBy == ItemSortBy.CriticRating ||
o.OrderBy == ItemSortBy.Runtime))
{
return false;
}
return true;
}

View File

@@ -3,8 +3,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Diacritics.Extensions;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Model.Entities;
@@ -230,9 +233,9 @@ namespace MediaBrowser.Controller.Entities
public int? IndexNumber { get; set; }
public int? MinParentalRating { get; set; }
public ParentalRatingScore? MinParentalRating { get; set; }
public int? MaxParentalRating { get; set; }
public ParentalRatingScore? MaxParentalRating { get; set; }
public bool? HasDeadParentId { get; set; }
@@ -304,6 +307,8 @@ namespace MediaBrowser.Controller.Entities
public bool? IsDeadStudio { get; set; }
public bool? IsDeadGenre { get; set; }
public bool? IsDeadPerson { get; set; }
/// <summary>
@@ -358,18 +363,26 @@ namespace MediaBrowser.Controller.Entities
public void SetUser(User user)
{
MaxParentalRating = user.MaxParentalAgeRating;
if (MaxParentalRating.HasValue)
var maxRating = user.MaxParentalRatingScore;
if (maxRating.HasValue)
{
string other = UnratedItem.Other.ToString();
BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
.Where(i => i != other)
.Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
MaxParentalRating = new(maxRating.Value, user.MaxParentalRatingSubScore);
}
ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags);
var other = UnratedItem.Other.ToString();
BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
.Where(i => i != other)
.Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
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;
}

View File

@@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
namespace MediaBrowser.Controller.Entities
{

View File

@@ -7,8 +7,10 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Querying;
@@ -195,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();

View File

@@ -15,6 +15,8 @@ namespace MediaBrowser.Controller.Entities
ArgumentNullException.ThrowIfNull(person);
ArgumentException.ThrowIfNullOrEmpty(person.Name);
person.Name = person.Name.Trim();
// Normalize
if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
{

View File

@@ -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))

View File

@@ -7,8 +7,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using System.Threading;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller.Dto;
@@ -152,6 +153,21 @@ namespace MediaBrowser.Controller.Entities.TV
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
if (SourceType == SourceType.Channel)
{
try
{
query.Parent = this;
query.ChannelIds = new[] { ChannelId };
return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
}
catch
{
// Already logged at lower levels
return new QueryResult<BaseItem>();
}
}
if (query.User is null)
{
return base.GetItemsInternal(query);
@@ -163,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>
@@ -257,7 +273,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
{
IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path);
IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId);
// If a change was made record it
if (IndexNumber.HasValue)

View File

@@ -9,12 +9,13 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
@@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// Class Series.
/// </summary>
public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer
public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer, ISupportsBoxSetGrouping
{
public Series()
{
@@ -213,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)
{
@@ -225,16 +226,27 @@ namespace MediaBrowser.Controller.Entities.TV
{
var user = query.User;
if (SourceType == SourceType.Channel)
{
try
{
query.Parent = this;
query.ChannelIds = [ChannelId];
return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
}
catch
{
// Already logged at lower levels
return new QueryResult<BaseItem>();
}
}
if (query.Recursive)
{
var seriesKey = GetUniqueSeriesKey(this);
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
if (query.OrderBy.Count == 0)
{
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
}
if (query.IncludeItemTypes.Length == 0)
{
@@ -371,7 +383,25 @@ namespace MediaBrowser.Controller.Entities.TV
query.IsMissing = false;
}
var allItems = LibraryManager.GetItemList(query);
IReadOnlyList<BaseItem> allItems;
if (SourceType == SourceType.Channel)
{
try
{
query.Parent = parentSeason;
query.ChannelIds = [ChannelId];
allItems = [.. ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult().Items];
}
catch
{
// Already logged at lower levels
return [];
}
}
else
{
allItems = LibraryManager.GetItemList(query);
}
return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
}

View File

@@ -8,7 +8,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
@@ -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)

View File

@@ -8,8 +8,8 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.TV;

View File

@@ -6,8 +6,10 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
@@ -436,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();
@@ -922,6 +920,11 @@ namespace MediaBrowser.Controller.Entities
}
}
if (query.ExcludeItemIds.Contains(item.Id))
{
return false;
}
return true;
}

View File

@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
namespace MediaBrowser.Controller
{

View File

@@ -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);
}

View File

@@ -0,0 +1,71 @@
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.IO;
/// <summary>
/// Interface IPathManager.
/// </summary>
public interface IPathManager
{
/// <summary>
/// Gets the path to the trickplay image base folder.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
/// <returns>The absolute path.</returns>
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false);
/// <summary>
/// Gets the path to the subtitle file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="streamIndex">The stream index.</param>
/// <param name="extension">The subtitle file extension.</param>
/// <returns>The absolute path.</returns>
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
/// <summary>
/// Gets the path to the subtitle file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <returns>The absolute path.</returns>
public string GetSubtitleFolderPath(string mediaSourceId);
/// <summary>
/// Gets the path to the attachment file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="fileName">The attachmentFileName index.</param>
/// <returns>The absolute path.</returns>
public string GetAttachmentPath(string mediaSourceId, string fileName);
/// <summary>
/// Gets the path to the attachment folder.
/// </summary>
/// <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);
}

View File

@@ -38,6 +38,11 @@ namespace MediaBrowser.Controller
/// <value>The name of the friendly.</value>
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>

View File

@@ -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

View File

@@ -1,5 +1,6 @@
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace MediaBrowser.Controller;
@@ -31,4 +32,10 @@ public interface ISystemManager
/// Starts the application shutdown process.
/// </summary>
void Shutdown();
/// <summary>
/// Gets the systems storage resources.
/// </summary>
/// <returns>The <see cref="SystemStorageInfo"/>.</returns>
SystemStorageInfo GetSystemStorageInfo();
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Library
@@ -23,6 +24,6 @@ namespace MediaBrowser.Controller.Library
/// <param name="item">The item.</param>
/// <param name="user">The user.</param>
/// <returns>IEnumerable{System.String}.</returns>
Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, Jellyfin.Data.Entities.User user);
Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user);
}
}

View File

@@ -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);
}

View File

@@ -4,8 +4,9 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -219,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.
@@ -426,8 +427,9 @@ namespace MediaBrowser.Controller.Library
/// Gets the season number from path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="parentId">The parent id.</param>
/// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
int? GetSeasonNumberFromPath(string path);
int? GetSeasonNumberFromPath(string path, Guid? parentId);
/// <summary>
/// Fills the missing episode numbers from path.
@@ -565,6 +567,24 @@ namespace MediaBrowser.Controller.Library
/// <returns>List of items.</returns>
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents);
/// <summary>
/// Gets the TVShow/Album items for Latest api.
/// </summary>
/// <param name="query">The query to use.</param>
/// <param name="parents">Items to use for query.</param>
/// <param name="collectionType">Collection Type.</param>
/// <returns>List of items.</returns>
IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, CollectionType collectionType);
/// <summary>
/// Gets the list of series presentation keys for next up.
/// </summary>
/// <param name="query">The query to use.</param>
/// <param name="parents">Items to use for query.</param>
/// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param>
/// <returns>List of series presentation keys.</returns>
IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff);
/// <summary>
/// Gets the items result.
/// </summary>
@@ -573,11 +593,11 @@ namespace MediaBrowser.Controller.Library
QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query);
/// <summary>
/// Ignores the file.
/// Checks if the file is ignored.
/// </summary>
/// <param name="file">The file.</param>
/// <param name="parent">The parent.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
/// <returns><c>true</c> if ignored, <c>false</c> otherwise.</returns>
bool IgnoreFile(FileSystemMetadata file, BaseItem parent);
Guid GetStudioId(string name);

View File

@@ -6,7 +6,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;

View File

@@ -1,7 +1,7 @@
#pragma warning disable CA1002, CS1591
using System.Collections.Generic;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;

View File

@@ -3,8 +3,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Users;

View File

@@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;

View File

@@ -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);
}

View File

@@ -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; }
}
}

View File

@@ -6,8 +6,8 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;

View File

@@ -18,6 +18,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BitFaster.Caching" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="System.Threading.Tasks.Dataflow" />
@@ -27,6 +28,7 @@
<ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" />
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
<ProjectReference Include="../src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,32 @@
namespace MediaBrowser.Controller.MediaEncoding;
/// <summary>
/// Enum BitStreamFilterOptionType.
/// </summary>
public enum BitStreamFilterOptionType
{
/// <summary>
/// hevc_metadata bsf with remove_dovi option.
/// </summary>
HevcMetadataRemoveDovi = 0,
/// <summary>
/// hevc_metadata bsf with remove_hdr10plus option.
/// </summary>
HevcMetadataRemoveHdr10Plus = 1,
/// <summary>
/// av1_metadata bsf with remove_dovi option.
/// </summary>
Av1MetadataRemoveDovi = 2,
/// <summary>
/// av1_metadata bsf with remove_hdr10plus option.
/// </summary>
Av1MetadataRemoveHdr10Plus = 3,
/// <summary>
/// dovi_rpu bsf with strip option.
/// </summary>
DoviRpuStrip = 4,
}

View File

@@ -13,10 +13,13 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
@@ -35,7 +38,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
/// This should matches all common valid codecs.
/// </summary>
public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
/// <summary>
/// The level validation regex.
/// This regular expression matches strings representing a double.
/// </summary>
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
private const string _defaultMjpegEncoder = "mjpeg";
@@ -53,6 +62,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager;
private readonly IPathManager _pathManager;
// i915 hang was fixed by linux 6.2 (3f882f2)
private readonly Version _minKerneli915Hang = new Version(5, 18);
@@ -74,8 +84,9 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1);
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
private static readonly string[] _videoProfilesH264 =
[
@@ -151,13 +162,22 @@ namespace MediaBrowser.Controller.MediaEncoding
IMediaEncoder mediaEncoder,
ISubtitleEncoder subtitleEncoder,
IConfiguration config,
IConfigurationManager configurationManager)
IConfigurationManager configurationManager,
IPathManager pathManager)
{
_appPaths = appPaths;
_mediaEncoder = mediaEncoder;
_subtitleEncoder = subtitleEncoder;
_config = config;
_configurationManager = configurationManager;
_pathManager = pathManager;
}
private enum DynamicHdrMetadataRemovalPlan
{
None,
RemoveDovi,
RemoveHdr10Plus,
}
[GeneratedRegex(@"\s+")]
@@ -210,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;
}
@@ -330,8 +350,17 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.VideoStream.VideoRange == VideoRange.HDR
&& state.VideoStream.VideoRangeType == VideoRangeType.DOVI)
{
// Only native SW decoder and HW accelerator can parse dovi rpu.
// Only native SW decoder, HW accelerator and hevc_rkmpp decoder can parse dovi rpu.
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
var isRkmppDecoder = vidDecoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
if (isRkmppDecoder
&& _mediaEncoder.EncoderVersion >= _minFFmpegRkmppHevcDecDoviRpu
&& string.Equals(state.VideoStream?.Codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
return true;
}
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
@@ -340,11 +369,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder;
}
return state.VideoStream.VideoRange == VideoRange.HDR
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|| state.VideoStream.VideoRangeType == VideoRangeType.HLG
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG);
// GPU tonemapping supports all HDR RangeTypes
return state.VideoStream.VideoRange == VideoRange.HDR;
}
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -379,8 +405,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
return state.VideoStream.VideoRange == VideoRange.HDR
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10);
&& IsDoviWithHdr10Bl(state.VideoStream);
}
private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -395,7 +420,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
// All other HDR formats working.
return state.VideoStream.VideoRange == VideoRange.HDR
&& state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG;
&& (IsDoviWithHdr10Bl(state.VideoStream)
|| state.VideoStream.VideoRangeType is VideoRangeType.HLG);
}
private bool IsVideoStreamHevcRext(EncodingJobInfo state)
@@ -450,7 +476,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetMjpegEncoder(state, encodingOptions);
}
if (_validationRegex.IsMatch(codec))
if (_containerValidationRegex.IsMatch(codec))
{
return codec.ToLowerInvariant();
}
@@ -491,7 +517,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetInputFormat(string container)
{
if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container))
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
{
return null;
}
@@ -709,7 +735,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = state.OutputAudioCodec;
if (!_validationRegex.IsMatch(codec))
if (!_containerValidationRegex.IsMatch(codec))
{
codec = "aac";
}
@@ -860,9 +886,9 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.EncoderVersion >= _minFFmpegVaapiDeviceVendorId;
// Priority: 'renderNodePath' > 'vendorId' > 'kernelDriver'
var driverOpts = string.IsNullOrEmpty(renderNodePath)
? (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}"))
: renderNodePath;
var driverOpts = File.Exists(renderNodePath)
? renderNodePath
: (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}"));
// 'driver' behaves similarly to env LIBVA_DRIVER_NAME
driverOpts += string.IsNullOrEmpty(driver) ? string.Empty : ",driver=" + driver;
@@ -1299,6 +1325,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.Contains("hevc", StringComparison.OrdinalIgnoreCase);
}
public static bool IsAv1(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
return codec.Contains("av1", StringComparison.OrdinalIgnoreCase);
}
public static bool IsAAC(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
@@ -1306,8 +1339,125 @@ namespace MediaBrowser.Controller.MediaEncoding
return codec.Contains("aac", StringComparison.OrdinalIgnoreCase);
}
public static string GetBitStreamArgs(MediaStream stream)
public static bool IsDoviWithHdr10Bl(MediaStream stream)
{
var rangeType = stream?.VideoRangeType;
return rangeType is VideoRangeType.DOVIWithHDR10
or VideoRangeType.DOVIWithEL
or VideoRangeType.DOVIWithHDR10Plus
or VideoRangeType.DOVIWithELHDR10Plus
or VideoRangeType.DOVIInvalid;
}
public static bool IsDovi(MediaStream stream)
{
var rangeType = stream?.VideoRangeType;
return IsDoviWithHdr10Bl(stream)
|| (rangeType is VideoRangeType.DOVI
or VideoRangeType.DOVIWithHLG
or VideoRangeType.DOVIWithSDR);
}
public static bool IsHdr10Plus(MediaStream stream)
{
var rangeType = stream?.VideoRangeType;
return rangeType is VideoRangeType.HDR10Plus
or VideoRangeType.DOVIWithHDR10Plus
or VideoRangeType.DOVIWithELHDR10Plus;
}
/// <summary>
/// Check if dynamic HDR metadata should be removed during stream copy.
/// Please note this check assumes the range check has already been done
/// and trivial fallbacks like HDR10+ to HDR10, DOVIWithHDR10 to HDR10 is already checked.
/// </summary>
private static DynamicHdrMetadataRemovalPlan ShouldRemoveDynamicHdrMetadata(EncodingJobInfo state)
{
var videoStream = state.VideoStream;
if (videoStream.VideoRange is not VideoRange.HDR)
{
return DynamicHdrMetadataRemovalPlan.None;
}
var requestedRangeTypes = state.GetRequestedRangeTypes(state.VideoStream.Codec);
if (requestedRangeTypes.Length == 0)
{
return DynamicHdrMetadataRemovalPlan.None;
}
var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVIwithEL = requestedRangeTypes.Contains(VideoRangeType.DOVIWithEL.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVIwithELHDR10plus = requestedRangeTypes.Contains(VideoRangeType.DOVIWithELHDR10Plus.ToString(), StringComparison.OrdinalIgnoreCase);
var shouldRemoveHdr10Plus = false;
// Case 1: Client supports HDR10, does not support DOVI with EL but EL presets
var shouldRemoveDovi = (!requestHasDOVIwithEL && requestHasHDR10) && videoStream.VideoRangeType == VideoRangeType.DOVIWithEL;
// Case 2: Client supports DOVI, does not support broken DOVI config
// Client does not report DOVI support should be allowed to copy bad data for remuxing as HDR10 players would not crash
shouldRemoveDovi = shouldRemoveDovi || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIInvalid);
// Special case: we have a video with both EL and HDR10+
// If the client supports EL but not in the case of coexistence with HDR10+, remove HDR10+ for compatibility reasons.
// Otherwise, remove DOVI if the client is not a DOVI player
if (videoStream.VideoRangeType == VideoRangeType.DOVIWithELHDR10Plus)
{
shouldRemoveHdr10Plus = requestHasDOVIwithEL && !requestHasDOVIwithELHDR10plus;
shouldRemoveDovi = shouldRemoveDovi || !shouldRemoveHdr10Plus;
}
if (shouldRemoveDovi)
{
return DynamicHdrMetadataRemovalPlan.RemoveDovi;
}
// If the client is a Dolby Vision Player, remove the HDR10+ metadata to avoid playback issues
shouldRemoveHdr10Plus = shouldRemoveHdr10Plus || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10Plus);
return shouldRemoveHdr10Plus ? DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus : DynamicHdrMetadataRemovalPlan.None;
}
private bool CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan plan, MediaStream videoStream)
{
return plan switch
{
DynamicHdrMetadataRemovalPlan.RemoveDovi => _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.DoviRpuStrip)
|| (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi))
|| (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi)),
DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus => (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus))
|| (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus)),
_ => true,
};
}
public bool IsDoviRemoved(EncodingJobInfo state)
{
return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveDovi
&& CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveDovi, state.VideoStream);
}
public bool IsHdr10PlusRemoved(EncodingJobInfo state)
{
return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus
&& CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus, state.VideoStream);
}
public string GetBitStreamArgs(EncodingJobInfo state, MediaStreamType streamType)
{
if (state is null)
{
return null;
}
var stream = streamType switch
{
MediaStreamType.Audio => state.AudioStream,
MediaStreamType.Video => state.VideoStream,
_ => state.VideoStream
};
// TODO This is auto inserted into the mpegts mux so it might not be needed.
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
if (IsH264(stream))
@@ -1315,21 +1465,57 @@ namespace MediaBrowser.Controller.MediaEncoding
return "-bsf:v h264_mp4toannexb";
}
if (IsH265(stream))
{
return "-bsf:v hevc_mp4toannexb";
}
if (IsAAC(stream))
{
// Convert adts header(mpegts) to asc header(mp4).
return "-bsf:a aac_adtstoasc";
}
if (IsH265(stream))
{
var filter = "-bsf:v hevc_mp4toannexb";
// The following checks are not complete because the copy would be rejected
// if the encoder cannot remove required metadata.
// And if bsf is used, we must already be using copy codec.
switch (ShouldRemoveDynamicHdrMetadata(state))
{
default:
case DynamicHdrMetadataRemovalPlan.None:
break;
case DynamicHdrMetadataRemovalPlan.RemoveDovi:
filter += _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi)
? ",hevc_metadata=remove_dovi=1"
: ",dovi_rpu=strip=1";
break;
case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus:
filter += ",hevc_metadata=remove_hdr10plus=1";
break;
}
return filter;
}
if (IsAv1(stream))
{
switch (ShouldRemoveDynamicHdrMetadata(state))
{
default:
case DynamicHdrMetadataRemovalPlan.None:
return null;
case DynamicHdrMetadataRemovalPlan.RemoveDovi:
return _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi)
? "-bsf:v av1_metadata=remove_dovi=1"
: "-bsf:v dovi_rpu=strip=1";
case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus:
return "-bsf:v av1_metadata=remove_hdr10plus=1";
}
}
return null;
}
public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
public string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
{
var bitStreamArgs = string.Empty;
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
@@ -1340,7 +1526,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
{
bitStreamArgs = GetBitStreamArgs(state.AudioStream);
bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Audio);
bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
}
@@ -1386,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))
@@ -1619,7 +1825,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var alphaParam = enableAlpha ? ":alpha=1" : string.Empty;
var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty;
var fontPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
var fontPath = _pathManager.GetAttachmentFolderPath(state.MediaSource.Id);
var fontParam = string.Format(
CultureInfo.InvariantCulture,
":fontsdir='{0}'",
@@ -2167,17 +2373,37 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// DOVIWithHDR10 should be compatible with HDR10 supporting players. Same goes with HLG and of course SDR. So allow copy of those formats
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)
|| (requestHasHLG && videoStream.VideoRangeType == VideoRangeType.DOVIWithHLG)
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)))
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
{
return false;
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG)
{
return false;
}
// Check complicated cases where we need to remove dynamic metadata
// Conservatively refuse to copy if the encoder can't remove dynamic metadata,
// but a removal is required for compatability reasons.
var dynamicHdrMetadataRemovalPlan = ShouldRemoveDynamicHdrMetadata(state);
if (!CanEncoderRemoveDynamicHdrMetadata(dynamicHdrMetadataRemovalPlan, videoStream))
{
return false;
}
}
}
@@ -2689,10 +2915,10 @@ namespace MediaBrowser.Controller.MediaEncoding
var seekTick = isHlsRemuxing ? time + 5000000L : time;
// Seeking beyond EOF makes no sense in transcoding. Clamp the seekTick value to
// [0, RuntimeTicks - 0.5s], so that the muxer gets packets and avoid error codes.
// [0, RuntimeTicks - 5.0s], so that the muxer gets packets and avoid error codes.
if (maxTime > 0)
{
seekTick = Math.Clamp(seekTick, 0, Math.Max(maxTime - 5000000L, 0));
seekTick = Math.Clamp(seekTick, 0, Math.Max(maxTime - 50000000L, 0));
}
seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick));
@@ -3278,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(
@@ -3754,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);
@@ -3771,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
@@ -3906,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)
@@ -3971,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);
@@ -3988,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");
}
@@ -4194,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}";
@@ -5619,7 +5880,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doOclTonemap = IsHwTonemapAvailable(state, options);
var hasSubs = state.SubtitleStream != null && ShouldEncodeSubtitle(state);
var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
@@ -5699,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);
}
@@ -5777,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");
}
@@ -5789,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");
@@ -5798,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)
@@ -6619,6 +6888,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var isAv1SupportedSwFormatsVt = is8_10bitSwFormatsVt || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
// The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment.
bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported();
@@ -6652,6 +6922,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
}
if (string.Equals("av1", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
&& isAv1SupportedSwFormatsVt
&& _mediaEncoder.IsVideoToolboxAv1DecodeAvailable)
{
return GetHwaccelType(state, options, "av1", bitDepth, useHwSurface);
}
}
return null;
@@ -6746,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);
}
}
@@ -6873,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);
}
@@ -6980,7 +7259,7 @@ namespace MediaBrowser.Controller.MediaEncoding
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
if (state.ReadInputAtNativeFramerate
if ((state.ReadInputAtNativeFramerate && !state.IsSegmentedLiveStream)
|| (mediaSource.Protocol == MediaProtocol.File
&& string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)))
{
@@ -7234,7 +7513,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
string bitStreamArgs = GetBitStreamArgs(state.VideoStream);
string bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Video);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;

View File

@@ -4,10 +4,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;

View File

@@ -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
}
}

View File

@@ -9,26 +9,33 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.MediaEncoding
namespace MediaBrowser.Controller.MediaEncoding;
public interface IAttachmentExtractor
{
public interface IAttachmentExtractor
{
Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment(
BaseItem item,
string mediaSourceId,
int attachmentStreamIndex,
CancellationToken cancellationToken);
/// <summary>
/// Gets the path to the attachment file.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="attachmentStreamIndex">The attachment index.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The async task.</returns>
Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment(
BaseItem item,
string mediaSourceId,
int attachmentStreamIndex,
CancellationToken cancellationToken);
Task ExtractAllAttachments(
string inputFile,
MediaSourceInfo mediaSource,
string outputPath,
CancellationToken cancellationToken);
Task ExtractAllAttachmentsExternal(
string inputArgument,
string id,
string outputPath,
CancellationToken cancellationToken);
}
/// <summary>
/// Gets the path to the attachment file.
/// </summary>
/// <param name="inputFile">The input file path.</param>
/// <param name="mediaSource">The <see cref="MediaSourceInfo" /> source id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The async task.</returns>
Task ExtractAllAttachments(
string inputFile,
MediaSourceInfo mediaSource,
CancellationToken cancellationToken);
}

View File

@@ -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);
}
}

View File

@@ -75,6 +75,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <value><c>true</c> if the Vaapi device supports vulkan drm interop, <c>false</c> otherwise.</value>
bool IsVaapiDeviceSupportVulkanDrmInterop { get; }
/// <summary>
/// Gets a value indicating whether av1 decoding is available via VideoToolbox.
/// </summary>
/// <value><c>true</c> if the av1 is available via VideoToolbox, <c>false</c> otherwise.</value>
bool IsVideoToolboxAv1DecodeAvailable { get; }
/// <summary>
/// Whether given encoder codec is supported.
/// </summary>
@@ -110,6 +116,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
bool SupportsFilterWithOption(FilterOptionType option);
/// <summary>
/// Whether the bitstream filter is supported with the given option.
/// </summary>
/// <param name="option">The option.</param>
/// <returns><c>true</c> if the bitstream filter is supported, <c>false</c> otherwise.</returns>
bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option);
/// <summary>
/// Extracts the audio image.
/// </summary>

View File

@@ -2,12 +2,13 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
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.

View File

@@ -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.

View File

@@ -1,6 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
namespace MediaBrowser.Controller.Net
{

View File

@@ -1,24 +0,0 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Untyped sync play command.
/// </summary>
public class SyncPlayGroupUpdateCommandMessage : OutboundWebSocketMessage<GroupUpdate>
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandMessage"/> class.
/// </summary>
/// <param name="data">The send command.</param>
public SyncPlayGroupUpdateCommandMessage(GroupUpdate data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@@ -1,25 +0,0 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sync play group update command with group info.
/// GroupUpdateTypes: GroupJoined.
/// </summary>
public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : OutboundWebSocketMessage<GroupUpdate<GroupInfoDto>>
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupInfoMessage"/> class.
/// </summary>
/// <param name="data">The group info.</param>
public SyncPlayGroupUpdateCommandOfGroupInfoMessage(GroupUpdate<GroupInfoDto> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@@ -1,25 +0,0 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sync play group update command with group state update.
/// GroupUpdateTypes: StateUpdate.
/// </summary>
public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : OutboundWebSocketMessage<GroupUpdate<GroupStateUpdate>>
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage"/> class.
/// </summary>
/// <param name="data">The group info.</param>
public SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage(GroupUpdate<GroupStateUpdate> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@@ -1,25 +0,0 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sync play group update command with play queue update.
/// GroupUpdateTypes: PlayQueue.
/// </summary>
public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : OutboundWebSocketMessage<GroupUpdate<PlayQueueUpdate>>
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage"/> class.
/// </summary>
/// <param name="data">The play queue update.</param>
public SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage(GroupUpdate<PlayQueueUpdate> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@@ -1,25 +0,0 @@
using System.ComponentModel;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
/// <summary>
/// Sync play group update command with string.
/// GroupUpdateTypes: GroupDoesNotExist (error), LibraryAccessDenied (error), NotInGroup (error), GroupLeft (groupId), UserJoined (username), UserLeft (username).
/// </summary>
public class SyncPlayGroupUpdateCommandOfStringMessage : OutboundWebSocketMessage<GroupUpdate<string>>
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfStringMessage"/> class.
/// </summary>
/// <param name="data">The send command.</param>
public SyncPlayGroupUpdateCommandOfStringMessage(GroupUpdate<string> data)
: base(data)
{
}
/// <inheritdoc />
[DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
}

View File

@@ -1,15 +1,20 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Chapters;
namespace MediaBrowser.Controller.Persistence;
/// <summary>
/// Interface IChapterManager.
/// Interface IChapterRepository.
/// </summary>
public interface IChapterRepository
{
/// <summary>
/// Deletes the chapters.
/// </summary>
/// <param name="itemId">The item.</param>
void DeleteChapters(Guid itemId);
/// <summary>
/// Saves the chapters.
/// </summary>
@@ -17,21 +22,6 @@ public interface IChapterRepository
/// <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);
/// <summary>
/// Gets a single chapter of a BaseItem on a specific index.
/// </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);
/// <summary>
/// Gets all chapters associated with the baseItem.
/// </summary>

View File

@@ -5,6 +5,8 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
@@ -59,6 +61,22 @@ public interface IItemRepository
/// <returns>List&lt;BaseItem&gt;.</returns>
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery filter);
/// <summary>
/// Gets the item list. Used mainly by the Latest api endpoint.
/// </summary>
/// <param name="filter">The query.</param>
/// <param name="collectionType">Collection Type.</param>
/// <returns>List&lt;BaseItem&gt;.</returns>
IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType);
/// <summary>
/// Gets the list of series presentation keys for next up.
/// </summary>
/// <param name="filter">The query.</param>
/// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param>
/// <returns>The list of keys.</returns>
IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff);
/// <summary>
/// Updates the inherited values.
/// </summary>
@@ -85,4 +103,11 @@ public interface IItemRepository
IReadOnlyList<string> GetGenreNames();
IReadOnlyList<string> GetAllArtistNames();
/// <summary>
/// Checks if an item has been persisted to the database.
/// </summary>
/// <param name="id">The id to check.</param>
/// <returns>True if the item exists, otherwise false.</returns>
Task<bool> ItemExistsAsync(Guid id);
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.MediaEncoding.Keyframes;
namespace MediaBrowser.Controller.Persistence;
/// <summary>
/// Provides methods for accessing keyframe data.
/// </summary>
public interface IKeyframeRepository
{
/// <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);
}

View File

@@ -9,8 +9,10 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;

View File

@@ -10,14 +10,15 @@ namespace MediaBrowser.Controller.Providers
{
public class DirectoryService : IDirectoryService
{
private readonly IFileSystem _fileSystem;
// TODO make static and switch to FastConcurrentLru.
private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new(StringComparer.Ordinal);
private readonly IFileSystem _fileSystem;
public DirectoryService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;

View File

@@ -31,12 +31,6 @@ namespace MediaBrowser.Controller.Providers
/// </remarks>
ExternalIdMediaType? Type { get; }
/// <summary>
/// Gets the URL format string for this id.
/// </summary>
[Obsolete("Obsolete in 10.10, to be removed in 10.11")]
string? UrlFormatString { get; }
/// <summary>
/// Determines whether this id supports a given item type.
/// </summary>

View File

@@ -6,7 +6,8 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Security;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -73,7 +74,7 @@ namespace MediaBrowser.Controller.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>A task containing the session information.</returns>
Task<SessionInfo> LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
Task<SessionInfo> LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user);
/// <summary>
/// Used to report that a session controller has connected.
@@ -160,7 +161,7 @@ namespace MediaBrowser.Controller.Session
/// <param name="sessionId">The identifier of the session.</param>
/// <param name="command">The group update.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <typeparam name="T">Type of group.</typeparam>
/// <typeparam name="T">The group update type.</typeparam>
/// <returns>Task.</returns>
Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken);
@@ -341,5 +342,13 @@ namespace MediaBrowser.Controller.Session
Task RevokeUserTokens(Guid userId, string currentAccessToken);
Task CloseIfNeededAsync(SessionInfo session);
/// <summary>
/// Used to close the livestream if needed.
/// </summary>
/// <param name="liveStreamId">The livestream id.</param>
/// <param name="sessionIdOrPlaySessionId">The session id or playsession id.</param>
/// <returns>Task.</returns>
Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId);
}
}

View File

@@ -1,5 +1,6 @@
#nullable disable
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Sorting
@@ -13,7 +14,7 @@ namespace MediaBrowser.Controller.Sorting
/// Gets or sets the user.
/// </summary>
/// <value>The user.</value>
Jellyfin.Data.Entities.User User { get; set; }
User User { get; set; }
/// <summary>
/// Gets or sets the user manager.
@@ -25,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; }
}
}

View File

@@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
}
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RemoveItems);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
if (playingItemRemoved && !context.PlayQueue.IsItemPlaying())
@@ -106,7 +106,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
}
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.MoveItem);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
}
@@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
_ => PlayQueueUpdateReason.Queue
};
var playQueueUpdate = context.GetPlayQueueUpdate(reason);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
}
@@ -184,7 +184,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
{
context.SetRepeatMode(request.Mode);
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RepeatMode);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
}
@@ -193,7 +193,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
{
context.SetShuffleMode(request.Mode);
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.ShuffleMode);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
}
@@ -221,7 +221,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
{
// Notify relevant state change event.
var stateUpdate = new GroupStateUpdate(Type, reason.Action);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.StateUpdate, stateUpdate);
var update = new SyncPlayStateUpdate(context.GroupId, stateUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
}

View File

@@ -78,7 +78,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
// Prepare new session.
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken);
context.SetBuffering(session, true);
@@ -152,7 +152,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
}
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
// Reset status of sessions and await for all Ready events.
@@ -177,7 +177,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
if (result)
{
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
// Reset status of sessions and await for all Ready events.
@@ -215,7 +215,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
context.RestartCurrentItem();
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
// Reset status of sessions and await for all Ready events.
@@ -336,7 +336,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
_logger.LogDebug("Session {SessionId} reported wrong playlist item in group {GroupId}.", session.Id, context.GroupId.ToString());
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem);
var updateSession = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var updateSession = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
context.SetBuffering(session, true);
@@ -410,7 +410,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
_logger.LogDebug("Session {SessionId} reported wrong playlist item in group {GroupId}.", session.Id, context.GroupId.ToString());
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken);
context.SetBuffering(session, true);
@@ -583,7 +583,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
{
// Send playing-queue update.
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NextItem);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
// Reset status of sessions and await for all Ready events.
@@ -629,7 +629,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
{
// Send playing-queue update.
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.PreviousItem);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate);
context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
// Reset status of sessions and await for all Ready events.

View File

@@ -66,11 +66,11 @@ namespace MediaBrowser.Controller.SyncPlay
/// <summary>
/// Sends a GroupUpdate message to the interested sessions.
/// </summary>
/// <typeparam name="T">The type of the data of the message.</typeparam>
/// <param name="from">The current session.</param>
/// <param name="type">The filtering type.</param>
/// <param name="message">The message to send.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <typeparam name="T">The group update type.</typeparam>
/// <returns>The task.</returns>
Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken);
@@ -91,15 +91,6 @@ namespace MediaBrowser.Controller.SyncPlay
/// <returns>The command.</returns>
SendCommand NewSyncPlayCommand(SendCommandType type);
/// <summary>
/// Builds a new group update message.
/// </summary>
/// <typeparam name="T">The type of the data of the message.</typeparam>
/// <param name="type">The update type.</param>
/// <param name="data">The data to send.</param>
/// <returns>The group update.</returns>
GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data);
/// <summary>
/// Sanitizes the PositionTicks, considers the current playing item when available.
/// </summary>

View File

@@ -20,7 +20,8 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session that's creating the group.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken);
/// <returns>The newly created group.</returns>
GroupInfoDto NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken);
/// <summary>
/// Adds the session to a group.
@@ -46,6 +47,14 @@ namespace MediaBrowser.Controller.SyncPlay
/// <returns>The list of available groups.</returns>
List<GroupInfoDto> ListGroups(SessionInfo session, ListGroupsRequest request);
/// <summary>
/// Gets available groups for a session by id.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="groupId">The group id.</param>
/// <returns>The groups or null.</returns>
GroupInfoDto GetGroup(SessionInfo session, Guid groupId);
/// <summary>
/// Handle a request by a session in a group.
/// </summary>

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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);
}

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
@@ -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.
@@ -58,6 +58,14 @@ public interface ITrickplayManager
/// <returns>Task.</returns>
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>
@@ -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.