aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2025-08-03 17:27:17 -0400
committerGitHub <noreply@github.com>2025-08-03 17:27:17 -0400
commit4b6fb6c4bb2478badad068ce18aabe0c2955db48 (patch)
tree15f986ee62327cceb8f5c8f009bcf08d10cfaa66 /MediaBrowser.Controller
parente7bc86ebb8496615e0b3f73eb4f13ab4c0913dc8 (diff)
parentdb7465e83d9cc07134a0bffad7ed17b1c7b873da (diff)
Merge branch 'master' into master
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs2
-rw-r--r--MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs2
-rw-r--r--MediaBrowser.Controller/Channels/Channel.cs5
-rw-r--r--MediaBrowser.Controller/Chapters/IChapterManager.cs55
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs2
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs4
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs2
-rw-r--r--MediaBrowser.Controller/Dto/DtoOptions.cs2
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs10
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs133
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs14
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs101
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs37
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs6
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs39
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs22
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs48
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs2
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs21
-rw-r--r--MediaBrowser.Controller/IDisplayPreferencesManager.cs2
-rw-r--r--MediaBrowser.Controller/IO/IExternalDataManager.cs19
-rw-r--r--MediaBrowser.Controller/IO/IPathManager.cs71
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs5
-rw-r--r--MediaBrowser.Controller/IServerApplicationPaths.cs4
-rw-r--r--MediaBrowser.Controller/ISystemManager.cs7
-rw-r--r--MediaBrowser.Controller/Library/IIntroProvider.cs3
-rw-r--r--MediaBrowser.Controller/Library/IKeyframeManager.cs37
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs32
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IMusicManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/LibraryTaskScheduler/ILimitedConcurrencyLibraryScheduler.cs23
-rw-r--r--MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs335
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs2
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs32
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs369
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs3
-rw-r--r--MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs12
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs47
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs28
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs13
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs28
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs6
-rw-r--r--MediaBrowser.Controller/Net/AuthorizationInfo.cs2
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs25
-rw-r--r--MediaBrowser.Controller/Persistence/IChapterRepository.cs (renamed from MediaBrowser.Controller/Chapters/IChapterRepository.cs)26
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs25
-rw-r--r--MediaBrowser.Controller/Persistence/IKeyframeRepository.cs37
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs4
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs5
-rw-r--r--MediaBrowser.Controller/Providers/IExternalId.cs6
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs15
-rw-r--r--MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs5
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs12
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs16
-rw-r--r--MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs11
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs11
-rw-r--r--MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs34
-rw-r--r--MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs29
-rw-r--r--MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs15
-rw-r--r--MediaBrowser.Controller/SystemBackupService/IBackupService.cs48
-rw-r--r--MediaBrowser.Controller/Trickplay/ITrickplayManager.cs14
75 files changed, 1570 insertions, 482 deletions
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
index 81b532fda..976a667ac 100644
--- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
@@ -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
diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
index 8c9d1baf8..592ce9955 100644
--- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
@@ -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
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
index 9e07000bc..199e22b3f 100644
--- a/MediaBrowser.Controller/Channels/Channel.cs
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs
new file mode 100644
index 000000000..7532e56c6
--- /dev/null
+++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs
@@ -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);
+}
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
index 38a78a67b..206b5ac42 100644
--- a/MediaBrowser.Controller/Collections/ICollectionManager.cs
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index fe7dc1cf9..ea38950d3 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index 702ce39a2..4eeec99b0 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs
index cb638cf90..a71cdbd62 100644
--- a/MediaBrowser.Controller/Dto/DtoOptions.cs
+++ b/MediaBrowser.Controller/Dto/DtoOptions.cs
@@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Dto
EnableUserData = true;
AddCurrentProgram = true;
- Fields = allFields ? AllItemFields : Array.Empty<ItemFields>();
+ Fields = allFields ? AllItemFields : [];
ImageTypes = AllImageTypes;
}
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index 22453f0f7..f1d507fcb 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index f3873775b..d016d8f62 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index ecb3ac3a6..58841e5b7 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -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());
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 55553da49..bb0b26b8e 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index dcd22a3b4..668e2c1e2 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -114,5 +114,19 @@ namespace MediaBrowser.Controller.Entities
source.DeepCopy(dest);
return dest;
}
+
+ /// <summary>
+ /// Determines if the item has changed.
+ /// </summary>
+ /// <param name="source">The source object.</param>
+ /// <param name="asOf">The timestamp to detect changes as of.</param>
+ /// <typeparam name="T">Source type.</typeparam>
+ /// <returns>Whether the item has changed.</returns>
+ public static bool HasChanged<T>(this T source, DateTime asOf)
+ where T : BaseItem
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ return source.DateModified.Subtract(asOf).Duration().TotalSeconds > 1;
+ }
}
}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index b7b5dac03..ca79e6245 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index af6348e46..06cbcc2e1 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -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,17 +1226,15 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.MinCommunityRating.HasValue)
- {
- return false;
- }
-
- if (request.MinCriticRating.HasValue)
+ if (request.MinIndexNumber.HasValue)
{
return false;
}
- if (request.MinIndexNumber.HasValue)
+ if (request.OrderBy.Any(o =>
+ o.OrderBy == ItemSortBy.CommunityRating ||
+ o.OrderBy == ItemSortBy.CriticRating ||
+ o.OrderBy == ItemSortBy.Runtime))
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 43f02fb72..b32b64f5d 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -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;
}
diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
index 3e1d89274..203a16a66 100644
--- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
@@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
-using Jellyfin.Data.Entities;
+using Jellyfin.Database.Implementations.Entities;
namespace MediaBrowser.Controller.Entities
{
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index c9a93d0f5..dd5852823 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -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();
diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs
index 4141b1712..24b1843ce 100644
--- a/MediaBrowser.Controller/Entities/PeopleHelper.cs
+++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs
@@ -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))
{
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 46bad3f3b..6bdba36f9 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -7,12 +7,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
+using System.Threading;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities.TV
@@ -22,6 +24,8 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries
{
+ public static IMediaEncoder MediaEncoder { get; set; }
+
/// <inheritdoc />
[JsonIgnore]
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
@@ -325,6 +329,39 @@ namespace MediaBrowser.Controller.Entities.TV
{
if (SourceType == SourceType.Library || SourceType == SourceType.LiveTV)
{
+ var libraryOptions = LibraryManager.GetLibraryOptions(this);
+ if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(Container, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ var mediaInfo = MediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = GetMediaSources(false)[0],
+ MediaType = DlnaProfileType.Video
+ },
+ CancellationToken.None).GetAwaiter().GetResult();
+ if (mediaInfo.ParentIndexNumber > 0)
+ {
+ ParentIndexNumber = mediaInfo.ParentIndexNumber;
+ }
+
+ if (mediaInfo.IndexNumber > 0)
+ {
+ IndexNumber = mediaInfo.IndexNumber;
+ }
+
+ if (!string.IsNullOrEmpty(mediaInfo.ShowName))
+ {
+ SeriesName = mediaInfo.ShowName;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", Path);
+ }
+ }
+
try
{
if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata))
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index e3fbe8e4d..48211d99f 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -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)
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 137d91f1c..62c73d56f 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -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);
}
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index 7ae4a4a2c..deed3631b 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -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)
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index f5ca3737c..dfa31315c 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 4ec2e4c0a..7679d383f 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -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;
}
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
index 10c0f56e0..a97096eae 100644
--- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using Jellyfin.Data.Entities;
+using Jellyfin.Database.Implementations.Entities;
namespace MediaBrowser.Controller
{
diff --git a/MediaBrowser.Controller/IO/IExternalDataManager.cs b/MediaBrowser.Controller/IO/IExternalDataManager.cs
new file mode 100644
index 000000000..f69f4586c
--- /dev/null
+++ b/MediaBrowser.Controller/IO/IExternalDataManager.cs
@@ -0,0 +1,19 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.IO;
+
+/// <summary>
+/// Interface IPathManager.
+/// </summary>
+public interface IExternalDataManager
+{
+ /// <summary>
+ /// Deletes all external item data.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs
new file mode 100644
index 000000000..eb6743754
--- /dev/null
+++ b/MediaBrowser.Controller/IO/IPathManager.cs
@@ -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);
+}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index e9c4d9e19..b76141db0 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -39,6 +39,11 @@ namespace MediaBrowser.Controller
string FriendlyName { get; }
/// <summary>
+ /// Gets or sets the path to the backup archive used to restore upon restart.
+ /// </summary>
+ string RestoreBackupPath { get; set; }
+
+ /// <summary>
/// Gets a URL specific for the request.
/// </summary>
/// <param name="request">The <see cref="HttpRequest"/> instance.</param>
diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs
index 608286cd8..a6e83a02c 100644
--- a/MediaBrowser.Controller/IServerApplicationPaths.cs
+++ b/MediaBrowser.Controller/IServerApplicationPaths.cs
@@ -2,6 +2,10 @@
#pragma warning disable CS1591
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using MediaBrowser.Common.Configuration;
namespace MediaBrowser.Controller
diff --git a/MediaBrowser.Controller/ISystemManager.cs b/MediaBrowser.Controller/ISystemManager.cs
index ef3034d2f..08344a1e5 100644
--- a/MediaBrowser.Controller/ISystemManager.cs
+++ b/MediaBrowser.Controller/ISystemManager.cs
@@ -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();
}
diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs
index 4a9721acb..860e948af 100644
--- a/MediaBrowser.Controller/Library/IIntroProvider.cs
+++ b/MediaBrowser.Controller/Library/IIntroProvider.cs
@@ -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);
}
}
diff --git a/MediaBrowser.Controller/Library/IKeyframeManager.cs b/MediaBrowser.Controller/Library/IKeyframeManager.cs
new file mode 100644
index 000000000..b0155efdd
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IKeyframeManager.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.MediaEncoding.Keyframes;
+
+namespace MediaBrowser.Controller.IO;
+
+/// <summary>
+/// Interface IKeyframeManager.
+/// </summary>
+public interface IKeyframeManager
+{
+ /// <summary>
+ /// Gets the keyframe data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <returns>The keyframe data.</returns>
+ IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId);
+
+ /// <summary>
+ /// Saves the keyframe data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="data">The keyframe data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The task object representing the asynchronous operation.</returns>
+ Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Deletes the keyframe data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The task object representing the asynchronous operation.</returns>
+ Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 47b1cb16e..98ed15eb6 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -4,8 +4,9 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -219,13 +220,13 @@ namespace MediaBrowser.Controller.Library
/// <param name="resolvers">The resolvers.</param>
/// <param name="introProviders">The intro providers.</param>
/// <param name="itemComparers">The item comparers.</param>
- /// <param name="postscanTasks">The postscan tasks.</param>
+ /// <param name="postScanTasks">The post scan tasks.</param>
void AddParts(
IEnumerable<IResolverIgnoreRule> rules,
IEnumerable<IItemResolver> resolvers,
IEnumerable<IIntroProvider> introProviders,
IEnumerable<IBaseItemComparer> itemComparers,
- IEnumerable<ILibraryPostScanTask> postscanTasks);
+ IEnumerable<ILibraryPostScanTask> postScanTasks);
/// <summary>
/// Sorts the specified items.
@@ -426,8 +427,9 @@ namespace MediaBrowser.Controller.Library
/// Gets the season number from path.
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="parentId">The parent id.</param>
/// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
- int? GetSeasonNumberFromPath(string path);
+ int? GetSeasonNumberFromPath(string path, Guid? parentId);
/// <summary>
/// Fills the missing episode numbers from path.
@@ -566,6 +568,24 @@ namespace MediaBrowser.Controller.Library
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>
/// <param name="query">The query.</param>
@@ -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);
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index eb697268c..2b6781a19 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs
index 7ba8fc20c..20764ec60 100644
--- a/MediaBrowser.Controller/Library/IMusicManager.cs
+++ b/MediaBrowser.Controller/Library/IMusicManager.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index 5a2deda66..eb46611dd 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index a4bd0f69e..7f06a318a 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
index 76e9eb1f5..b0a6782c7 100644
--- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
+++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
@@ -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;
diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/ILimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/ILimitedConcurrencyLibraryScheduler.cs
new file mode 100644
index 000000000..e7460a2e6
--- /dev/null
+++ b/MediaBrowser.Controller/LibraryTaskScheduler/ILimitedConcurrencyLibraryScheduler.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.LibraryTaskScheduler;
+
+/// <summary>
+/// Provides a shared scheduler to run library related tasks based on the <see cref="ServerConfiguration.LibraryScanFanoutConcurrency"/>.
+/// </summary>
+public interface ILimitedConcurrencyLibraryScheduler
+{
+ /// <summary>
+ /// Enqueues an action that will be invoked with the set data.
+ /// </summary>
+ /// <typeparam name="T">The data Type.</typeparam>
+ /// <param name="data">The data.</param>
+ /// <param name="worker">The callback to process the data.</param>
+ /// <param name="progress">A progress reporter.</param>
+ /// <param name="cancellationToken">Stop token.</param>
+ /// <returns>A task that finishes when all data has been processed by the worker.</returns>
+ Task Enqueue<T>(T[] data, Func<T, IProgress<double>, Task> worker, IProgress<double> progress, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs
new file mode 100644
index 000000000..0de5f198d
--- /dev/null
+++ b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs
@@ -0,0 +1,335 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.LibraryTaskScheduler;
+
+/// <summary>
+/// Provides Parallel action interface to process tasks with a set concurrency level.
+/// </summary>
+public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibraryScheduler, IAsyncDisposable
+{
+ private const int CleanupGracePeriod = 60;
+ private readonly IHostApplicationLifetime _hostApplicationLifetime;
+ private readonly ILogger<LimitedConcurrencyLibraryScheduler> _logger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly Dictionary<CancellationTokenSource, Task> _taskRunners = new();
+
+ private static readonly AsyncLocal<CancellationTokenSource> _deadlockDetector = new();
+
+ /// <summary>
+ /// Gets used to lock all operations on the Tasks queue and creating workers.
+ /// </summary>
+ private readonly Lock _taskLock = new();
+
+ private readonly BlockingCollection<TaskQueueItem> _tasks = new();
+
+ private volatile int _workCounter;
+ private Task? _cleanupTask;
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LimitedConcurrencyLibraryScheduler"/> class.
+ /// </summary>
+ /// <param name="hostApplicationLifetime">The hosting lifetime.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ public LimitedConcurrencyLibraryScheduler(
+ IHostApplicationLifetime hostApplicationLifetime,
+ ILogger<LimitedConcurrencyLibraryScheduler> logger,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _hostApplicationLifetime = hostApplicationLifetime;
+ _logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ private void ScheduleTaskCleanup()
+ {
+ lock (_taskLock)
+ {
+ if (_cleanupTask is not null)
+ {
+ _logger.LogDebug("Cleanup task already scheduled.");
+ // cleanup task is already running.
+ return;
+ }
+
+ _cleanupTask = RunCleanupTask();
+ }
+
+ async Task RunCleanupTask()
+ {
+ _logger.LogDebug("Schedule cleanup task in {CleanupGracePerioid} sec.", CleanupGracePeriod);
+ await Task.Delay(TimeSpan.FromSeconds(CleanupGracePeriod)).ConfigureAwait(false);
+ if (_disposed)
+ {
+ _logger.LogDebug("Abort cleaning up, already disposed.");
+ return;
+ }
+
+ lock (_taskLock)
+ {
+ if (_tasks.Count > 0 || _workCounter > 0)
+ {
+ _logger.LogDebug("Delay cleanup task, operations still running.");
+ // tasks are still there so its still in use. Reschedule cleanup task.
+ // we cannot just exit here and rely on the other invoker because there is a considerable timeframe where it could have already ended.
+ _cleanupTask = RunCleanupTask();
+ return;
+ }
+ }
+
+ _logger.LogDebug("Cleanup runners.");
+ foreach (var item in _taskRunners.ToArray())
+ {
+ await item.Key.CancelAsync().ConfigureAwait(false);
+ _taskRunners.Remove(item.Key);
+ }
+ }
+ }
+
+ private bool ShouldForceSequentialOperation()
+ {
+ // if the user either set the setting to 1 or it's unset and we have fewer than 4 cores it's better to run sequentially.
+ var fanoutSetting = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
+ return fanoutSetting == 1 || (fanoutSetting <= 0 && Environment.ProcessorCount <= 3);
+ }
+
+ private int CalculateScanConcurrencyLimit()
+ {
+ // when this is invoked, we already checked ShouldForceSequentialOperation for the sequential check.
+ var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
+ if (fanoutConcurrency <= 0)
+ {
+ // in case the user did not set a limit manually, we can assume he has 3 or more cores as already checked by ShouldForceSequentialOperation.
+ return Environment.ProcessorCount - 3;
+ }
+
+ return fanoutConcurrency;
+ }
+
+ private void Worker()
+ {
+ lock (_taskLock)
+ {
+ var operationFanout = Math.Max(0, CalculateScanConcurrencyLimit() - _taskRunners.Count);
+ _logger.LogDebug("Spawn {NumberRunners} new runners.", operationFanout);
+ for (int i = 0; i < operationFanout; i++)
+ {
+ var stopToken = new CancellationTokenSource();
+ var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, _hostApplicationLifetime.ApplicationStopping);
+ _taskRunners.Add(
+ combinedSource,
+ Task.Factory.StartNew(
+ ItemWorker,
+ (combinedSource, stopToken),
+ combinedSource.Token,
+ TaskCreationOptions.PreferFairness,
+ TaskScheduler.Default));
+ }
+ }
+ }
+
+ private async Task ItemWorker(object? obj)
+ {
+ var stopToken = ((CancellationTokenSource TaskStop, CancellationTokenSource GlobalStop))obj!;
+ _deadlockDetector.Value = stopToken.TaskStop;
+ try
+ {
+ foreach (var item in _tasks.GetConsumingEnumerable(stopToken.GlobalStop.Token))
+ {
+ stopToken.GlobalStop.Token.ThrowIfCancellationRequested();
+ try
+ {
+ var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0;
+ Debug.Assert(newWorkerLimit, "_workCounter > 0");
+ _logger.LogDebug("Process new item '{Data}'.", item.Data);
+ await ProcessItem(item).ConfigureAwait(false);
+ }
+ finally
+ {
+ var newWorkerLimit = Interlocked.Decrement(ref _workCounter) >= 0;
+ Debug.Assert(newWorkerLimit, "_workCounter > 0");
+ }
+ }
+ }
+ catch (OperationCanceledException) when (stopToken.TaskStop.IsCancellationRequested)
+ {
+ // thats how you do it, interupt the waiter thread. There is nothing to do here when it was on purpose.
+ }
+ finally
+ {
+ _logger.LogDebug("Cleanup Runner'.");
+ _deadlockDetector.Value = default!;
+ _taskRunners.Remove(stopToken.TaskStop);
+ stopToken.GlobalStop.Dispose();
+ stopToken.TaskStop.Dispose();
+ }
+ }
+
+ private async Task ProcessItem(TaskQueueItem item)
+ {
+ try
+ {
+ if (item.CancellationToken.IsCancellationRequested)
+ {
+ // if item is cancelled, just skip it
+ return;
+ }
+
+ await item.Worker(item.Data).ConfigureAwait(true);
+ }
+ catch (System.Exception ex)
+ {
+ _logger.LogError(ex, "Error while performing a library operation");
+ }
+ finally
+ {
+ item.Progress.Report(100);
+ item.Done.SetResult();
+ }
+ }
+
+ /// <inheritdoc/>
+ public async Task Enqueue<T>(T[] data, Func<T, IProgress<double>, Task> worker, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (data.Length == 0 || cancellationToken.IsCancellationRequested)
+ {
+ progress.Report(100);
+ return;
+ }
+
+ _logger.LogDebug("Enqueue new Workset of {NoItems} items.", data.Length);
+
+ TaskQueueItem[] workItems = null!;
+
+ void UpdateProgress()
+ {
+ progress.Report(workItems.Select(e => e.ProgressValue).Average());
+ }
+
+ workItems = data.Select(item =>
+ {
+ TaskQueueItem queueItem = null!;
+ return queueItem = new TaskQueueItem()
+ {
+ Data = item!,
+ Progress = new Progress<double>(innerPercent =>
+ {
+ // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls
+ var innerPercentRounded = Math.Round(innerPercent);
+ if (queueItem.ProgressValue != innerPercentRounded)
+ {
+ queueItem.ProgressValue = innerPercentRounded;
+ UpdateProgress();
+ }
+ }),
+ Worker = (val) => worker((T)val, queueItem.Progress),
+ CancellationToken = cancellationToken
+ };
+ }).ToArray();
+
+ if (ShouldForceSequentialOperation())
+ {
+ _logger.LogDebug("Process sequentially.");
+ try
+ {
+ foreach (var item in workItems)
+ {
+ await ProcessItem(item).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ // operation is cancelled. Do nothing.
+ }
+
+ _logger.LogDebug("Process sequentially done.");
+ return;
+ }
+
+ for (var i = 0; i < workItems.Length; i++)
+ {
+ var item = workItems[i]!;
+ _tasks.Add(item, CancellationToken.None);
+ }
+
+ if (_deadlockDetector.Value is not null)
+ {
+ _logger.LogDebug("Nested invocation detected, process in-place.");
+ try
+ {
+ // we are in a nested loop. There is no reason to spawn a task here as that would just lead to deadlocks and no additional concurrency is achieved
+ while (workItems.Any(e => !e.Done.Task.IsCompleted) && _tasks.TryTake(out var item, 200, _deadlockDetector.Value.Token))
+ {
+ await ProcessItem(item).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException) when (_deadlockDetector.Value.IsCancellationRequested)
+ {
+ // operation is cancelled. Do nothing.
+ }
+
+ _logger.LogDebug("process in-place done.");
+ }
+ else
+ {
+ Worker();
+ _logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length);
+ await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false);
+ _logger.LogDebug("{NoWorkers} completed.", workItems.Length);
+ ScheduleTaskCleanup();
+ }
+ }
+
+ /// <inheritdoc/>
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ _tasks.CompleteAdding();
+ foreach (var item in _taskRunners)
+ {
+ await item.Key.CancelAsync().ConfigureAwait(false);
+ }
+
+ _tasks.Dispose();
+ if (_cleanupTask is not null)
+ {
+ await _cleanupTask.ConfigureAwait(false);
+ _cleanupTask?.Dispose();
+ }
+ }
+
+ private class TaskQueueItem
+ {
+ public required object Data { get; init; }
+
+ public double ProgressValue { get; set; }
+
+ public required Func<object, Task> Worker { get; init; }
+
+ public required IProgress<double> Progress { get; init; }
+
+ public TaskCompletionSource Done { get; } = new();
+
+ public CancellationToken CancellationToken { get; init; }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index c0e46ba24..8d59eef9f 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -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;
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index ba4a2a59c..3353ad63f 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -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>
diff --git a/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs
new file mode 100644
index 000000000..41d21e440
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs
@@ -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,
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index a9e419df4..8d3977103 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -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 (IsAAC(stream))
+ {
+ // Convert adts header(mpegts) to asc header(mp4).
+ return "-bsf:a aac_adtstoasc";
+ }
+
if (IsH265(stream))
{
- return "-bsf:v hevc_mp4toannexb";
+ 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 (IsAAC(stream))
+ if (IsAv1(stream))
{
- // Convert adts header(mpegts) to asc header(mp4).
- return "-bsf:a aac_adtstoasc";
+ 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;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index caa312987..8d6211051 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -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;
diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
index a2b6e1d73..6ad953023 100644
--- a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
+++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
@@ -38,6 +38,16 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// The transpose_opencl_reversal.
/// </summary>
- TransposeOpenclReversal = 6
+ TransposeOpenclReversal = 6,
+
+ /// <summary>
+ /// The overlay_opencl_alpha_format.
+ /// </summary>
+ OverlayOpenclAlphaFormat = 7,
+
+ /// <summary>
+ /// The overlay_cuda_alpha_format.
+ /// </summary>
+ OverlayCudaAlphaFormat = 8
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
index 09840d2ee..d8d136472 100644
--- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
@@ -9,26 +9,33 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Controller.MediaEncoding
-{
- public interface IAttachmentExtractor
- {
- Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment(
- BaseItem item,
- string mediaSourceId,
- int attachmentStreamIndex,
- CancellationToken cancellationToken);
+namespace MediaBrowser.Controller.MediaEncoding;
- Task ExtractAllAttachments(
- string inputFile,
- MediaSourceInfo mediaSource,
- string outputPath,
- CancellationToken cancellationToken);
+public interface IAttachmentExtractor
+{
+ /// <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 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);
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
deleted file mode 100644
index 8ce40a58d..000000000
--- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.MediaEncoding
-{
- public interface IEncodingManager
- {
- /// <summary>
- /// Refreshes the chapter images.
- /// </summary>
- /// <param name="video">Video to use.</param>
- /// <param name="directoryService">Directory service to use.</param>
- /// <param name="chapters">Set of chapters to refresh.</param>
- /// <param name="extractImages">Option to extract images.</param>
- /// <param name="saveChapters">Option to save chapters.</param>
- /// <param name="cancellationToken">CancellationToken to use for operation.</param>
- /// <returns><c>true</c> if successful, <c>false</c> if not.</returns>
- Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index c767b4a51..de6353c4c 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -76,6 +76,12 @@ namespace MediaBrowser.Controller.MediaEncoding
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>
/// <param name="encoder">The encoder.</param>
@@ -111,6 +117,13 @@ namespace MediaBrowser.Controller.MediaEncoding
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>
/// <param name="path">The path.</param>
diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs
index 570d2bace..4f13a7ecc 100644
--- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs
+++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs
@@ -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.
diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
index 39bb58bef..5a6d15d78 100644
--- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
+++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
@@ -1,13 +1,11 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model;
using MediaBrowser.Model.MediaSegments;
-namespace MediaBrowser.Controller;
+namespace MediaBrowser.Controller.MediaSegments;
/// <summary>
/// Provides methods for Obtaining the Media Segments from an Item.
diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
index e452f2649..dd5eb9a01 100644
--- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs
+++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
@@ -1,6 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
-using Jellyfin.Data.Entities;
+using Jellyfin.Database.Implementations.Entities;
namespace MediaBrowser.Controller.Net
{
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs
deleted file mode 100644
index 6a501aa7e..000000000
--- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs
+++ /dev/null
@@ -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;
-}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs
deleted file mode 100644
index 47f706e2a..000000000
--- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs
+++ /dev/null
@@ -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;
-}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs
deleted file mode 100644
index 11ddb1e25..000000000
--- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs
+++ /dev/null
@@ -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;
-}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs
deleted file mode 100644
index 7e73399b1..000000000
--- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs
+++ /dev/null
@@ -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;
-}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs
deleted file mode 100644
index 5b5ccd3ed..000000000
--- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs
+++ /dev/null
@@ -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;
-}
diff --git a/MediaBrowser.Controller/Chapters/IChapterRepository.cs b/MediaBrowser.Controller/Persistence/IChapterRepository.cs
index e22cb0f58..0844ddb36 100644
--- a/MediaBrowser.Controller/Chapters/IChapterRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IChapterRepository.cs
@@ -1,36 +1,26 @@
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>
- /// Saves the chapters.
+ /// Deletes the chapters.
/// </summary>
/// <param name="itemId">The item.</param>
- /// <param name="chapters">The set of chapters.</param>
- void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
-
- /// <summary>
- /// Gets all chapters associated with the baseItem.
- /// </summary>
- /// <param name="baseItem">The baseitem.</param>
- /// <returns>A readonly list of chapter instances.</returns>
- IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem);
+ void DeleteChapters(Guid itemId);
/// <summary>
- /// Gets a single chapter of a BaseItem on a specific index.
+ /// Saves the chapters.
/// </summary>
- /// <param name="baseItem">The baseitem.</param>
- /// <param name="index">The index of that chapter.</param>
- /// <returns>A chapter instance.</returns>
- ChapterInfo? GetChapter(BaseItemDto baseItem, int index);
+ /// <param name="itemId">The item.</param>
+ /// <param name="chapters">The set of chapters.</param>
+ void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
/// <summary>
/// Gets all chapters associated with the baseItem.
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index afe2d833d..f4ac0ece4 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -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;
@@ -60,6 +62,22 @@ public interface IItemRepository
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery filter);
/// <summary>
+ /// Gets the item list. Used mainly by the Latest api endpoint.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <param name="collectionType">Collection Type.</param>
+ /// <returns>List&lt;BaseItem&gt;.</returns>
+ IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType);
+
+ /// <summary>
+ /// Gets the list of series presentation keys for next up.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param>
+ /// <returns>The list of keys.</returns>
+ IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff);
+
+ /// <summary>
/// Updates the inherited values.
/// </summary>
void UpdateInheritedValues();
@@ -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);
}
diff --git a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
new file mode 100644
index 000000000..2596784ba
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.MediaEncoding.Keyframes;
+
+namespace MediaBrowser.Controller.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);
+}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index edea54291..1062399e3 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 474f09dc5..a1edfa3c9 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -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;
diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs
index f451eac6d..584c3297a 100644
--- a/MediaBrowser.Controller/Providers/IExternalId.cs
+++ b/MediaBrowser.Controller/Providers/IExternalId.cs
@@ -32,12 +32,6 @@ namespace MediaBrowser.Controller.Providers
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>
/// <param name="item">The item.</param>
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 5dd0413b4..2b3afa117 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -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);
}
}
diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
index 66a0c5254..2206a021a 100644
--- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
@@ -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; }
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
index 51c95a1bb..31890c40a 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
@@ -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);
}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
index dcc06db1e..132765b71 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
@@ -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.
diff --git a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
index d2de22450..ddf86be71 100644
--- a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
+++ b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
@@ -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);
@@ -92,15 +92,6 @@ namespace MediaBrowser.Controller.SyncPlay
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>
/// <param name="positionTicks">The PositionTicks.</param>
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
index a6999a12c..6365a389e 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
@@ -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.
@@ -47,6 +48,14 @@ namespace MediaBrowser.Controller.SyncPlay
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>
/// <param name="session">The session.</param>
diff --git a/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs
new file mode 100644
index 000000000..b094ec275
--- /dev/null
+++ b/MediaBrowser.Controller/SystemBackupService/BackupManifestDto.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace MediaBrowser.Controller.SystemBackupService;
+
+/// <summary>
+/// Manifest type for backups internal structure.
+/// </summary>
+public class BackupManifestDto
+{
+ /// <summary>
+ /// Gets or sets the jellyfin version this backup was created with.
+ /// </summary>
+ public required Version ServerVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the backup engine version this backup was created with.
+ /// </summary>
+ public required Version BackupEngineVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date this backup was created with.
+ /// </summary>
+ public required DateTimeOffset DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path to the backup on the system.
+ /// </summary>
+ public required string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the contents of the backup archive.
+ /// </summary>
+ public required BackupOptionsDto Options { get; set; }
+}
diff --git a/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs
new file mode 100644
index 000000000..fc5a109f1
--- /dev/null
+++ b/MediaBrowser.Controller/SystemBackupService/BackupOptionsDto.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace MediaBrowser.Controller.SystemBackupService;
+
+/// <summary>
+/// Defines the optional contents of the backup archive.
+/// </summary>
+public class BackupOptionsDto
+{
+ /// <summary>
+ /// Gets or sets a value indicating whether the archive contains the Metadata contents.
+ /// </summary>
+ public bool Metadata { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the archive contains the Trickplay contents.
+ /// </summary>
+ public bool Trickplay { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the archive contains the Subtitle contents.
+ /// </summary>
+ public bool Subtitles { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the archive contains the Database contents.
+ /// </summary>
+ public bool Database { get; set; } = true;
+}
diff --git a/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs b/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs
new file mode 100644
index 000000000..263fa00c8
--- /dev/null
+++ b/MediaBrowser.Controller/SystemBackupService/BackupRestoreRequestDto.cs
@@ -0,0 +1,15 @@
+using System;
+using MediaBrowser.Common.Configuration;
+
+namespace MediaBrowser.Controller.SystemBackupService;
+
+/// <summary>
+/// Defines properties used to start a restore process.
+/// </summary>
+public class BackupRestoreRequestDto
+{
+ /// <summary>
+ /// Gets or Sets the name of the backup archive to restore from. Must be present in <see cref="IApplicationPaths.BackupPath"/>.
+ /// </summary>
+ public required string ArchiveFileName { get; set; }
+}
diff --git a/MediaBrowser.Controller/SystemBackupService/IBackupService.cs b/MediaBrowser.Controller/SystemBackupService/IBackupService.cs
new file mode 100644
index 000000000..0c586d811
--- /dev/null
+++ b/MediaBrowser.Controller/SystemBackupService/IBackupService.cs
@@ -0,0 +1,48 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.SystemBackupService;
+
+namespace Jellyfin.Server.Implementations.SystemBackupService;
+
+/// <summary>
+/// Defines an interface to restore and backup the jellyfin system.
+/// </summary>
+public interface IBackupService
+{
+ /// <summary>
+ /// Creates a new Backup zip file containing the current state of the application.
+ /// </summary>
+ /// <param name="backupOptions">The backup options.</param>
+ /// <returns>A task.</returns>
+ Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions);
+
+ /// <summary>
+ /// Gets a list of backups that are available to be restored from.
+ /// </summary>
+ /// <returns>A list of backup paths.</returns>
+ Task<BackupManifestDto[]> EnumerateBackups();
+
+ /// <summary>
+ /// Gets a single backup manifest if the path defines a valid Jellyfin backup archive.
+ /// </summary>
+ /// <param name="archivePath">The path to be loaded.</param>
+ /// <returns>The containing backup manifest or null if not existing or compatiable.</returns>
+ Task<BackupManifestDto?> GetBackupManifest(string archivePath);
+
+ /// <summary>
+ /// Restores an backup zip file created by jellyfin.
+ /// </summary>
+ /// <param name="archivePath">Path to the archive.</param>
+ /// <returns>A Task.</returns>
+ /// <exception cref="FileNotFoundException">Thrown when an invalid or missing file is specified.</exception>
+ /// <exception cref="NotSupportedException">Thrown when attempt to load an unsupported backup is made.</exception>
+ /// <exception cref="InvalidOperationException">Thrown for errors during the restore.</exception>
+ Task RestoreBackupAsync(string archivePath);
+
+ /// <summary>
+ /// Schedules a Restore and restarts the server.
+ /// </summary>
+ /// <param name="archivePath">The path to the archive to restore from.</param>
+ void ScheduleRestoreAndRestartServer(string archivePath);
+}
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
index 800317800..fba24329a 100644
--- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -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.
@@ -59,6 +59,14 @@ public interface ITrickplayManager
Task SaveTrickplayInfo(TrickplayInfo info);
/// <summary>
+ /// Deletes all trickplay info for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task DeleteTrickplayDataAsync(Guid itemId, CancellationToken cancellationToken);
+
+ /// <summary>
/// Gets all trickplay infos for all media streams of an item.
/// </summary>
/// <param name="item">The item.</param>
@@ -93,7 +101,7 @@ public interface ITrickplayManager
/// <param name="libraryOptions">The library options.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
/// <returns>Task.</returns>
- Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
+ Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions libraryOptions, CancellationToken cancellationToken);
/// <summary>
/// Gets the trickplay HLS playlist.