mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 08:23:28 -03:00
Merge branch 'master' into master
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
55
MediaBrowser.Controller/Chapters/IChapterManager.cs
Normal file
55
MediaBrowser.Controller/Chapters/IChapterManager.cs
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Dto
|
||||
EnableUserData = true;
|
||||
AddCurrentProgram = true;
|
||||
|
||||
Fields = allFields ? AllItemFields : Array.Empty<ItemFields>();
|
||||
Fields = allFields ? AllItemFields : [];
|
||||
ImageTypes = AllImageTypes;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller
|
||||
{
|
||||
|
||||
19
MediaBrowser.Controller/IO/IExternalDataManager.cs
Normal file
19
MediaBrowser.Controller/IO/IExternalDataManager.cs
Normal 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);
|
||||
}
|
||||
71
MediaBrowser.Controller/IO/IPathManager.cs
Normal file
71
MediaBrowser.Controller/IO/IPathManager.cs
Normal 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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
37
MediaBrowser.Controller/Library/IKeyframeManager.cs
Normal file
37
MediaBrowser.Controller/Library/IKeyframeManager.cs
Normal 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);
|
||||
}
|
||||
@@ -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<System.Int32>.</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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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<BaseItem>.</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<BaseItem>.</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);
|
||||
}
|
||||
|
||||
37
MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
Normal file
37
MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user