aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs12
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs68
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Extensions.cs8
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs68
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs21
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs17
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs19
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs51
-rw-r--r--MediaBrowser.Controller/Entities/TagExtensions.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs20
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs4
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs15
-rw-r--r--MediaBrowser.Controller/IO/FileSystemHelper.cs64
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs61
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs20
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs275
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs17
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs39
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs46
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs49
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs32
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs6
-rw-r--r--MediaBrowser.Controller/Providers/IExternalId.cs2
-rw-r--r--MediaBrowser.Controller/Providers/IExternalUrlProvider.cs22
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs12
-rw-r--r--MediaBrowser.Controller/Providers/ItemInfo.cs6
-rw-r--r--MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs2
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs5
40 files changed, 617 insertions, 386 deletions
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index b225f22df..40cdd6c91 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -155,11 +155,11 @@ namespace MediaBrowser.Controller.Entities
return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren);
}
- protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
ClearCache();
- await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken)
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken)
.ConfigureAwait(false);
ClearCache();
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index 237345206..a0aae8769 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -169,8 +169,7 @@ namespace MediaBrowser.Controller.Entities.Audio
var childUpdateType = ItemUpdateType.None;
- // Refresh songs only and not m3u files in album folder
- foreach (var item in items.OfType<Audio>())
+ foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -183,14 +182,13 @@ namespace MediaBrowser.Controller.Entities.Audio
progress.Report(percent * 95);
}
- // get album LUFS
- LUFS = items.OfType<Audio>().Max(item => item.LUFS);
-
var parentRefreshOptions = refreshOptions;
if (childUpdateType > ItemUpdateType.None)
{
- parentRefreshOptions = new MetadataRefreshOptions(refreshOptions);
- parentRefreshOptions.MetadataRefreshMode = MetadataRefreshMode.FullRefresh;
+ parentRefreshOptions = new MetadataRefreshOptions(refreshOptions)
+ {
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh
+ };
}
// Refresh current item
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 11cdf8444..1ab6c9706 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -110,7 +110,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return base.IsSaveLocalMetadataEnabled();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (IsAccessedByName)
{
@@ -118,7 +118,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return Task.CompletedTask;
}
- return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken);
+ return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken);
}
public override List<string> GetUserDataKeys()
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index ac9698ec9..7b6f364f7 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -135,7 +135,14 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <value>The LUFS Value.</value>
[JsonIgnore]
- public float LUFS { get; set; }
+ public float? LUFS { get; set; }
+
+ /// <summary>
+ /// Gets or sets the gain required for audio normalization.
+ /// </summary>
+ /// <value>The gain required for audio normalization.</value>
+ [JsonIgnore]
+ public float? NormalizationGain { get; set; }
/// <summary>
/// Gets or sets the channel identifier.
@@ -745,9 +752,6 @@ namespace MediaBrowser.Controller.Entities
public virtual bool SupportsAncestors => true;
[JsonIgnore]
- public virtual bool StopRefreshIfLocalMetadataFound => true;
-
- [JsonIgnore]
protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
[JsonIgnore]
@@ -833,7 +837,7 @@ namespace MediaBrowser.Controller.Entities
return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
}
- public bool CanDelete(User user)
+ public virtual bool CanDelete(User user)
{
var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
@@ -1602,6 +1606,12 @@ namespace MediaBrowser.Controller.Entities
return false;
}
+ var parent = GetParents().FirstOrDefault() ?? this;
+ if (parent is UserRootFolder or AggregateFolder)
+ {
+ return true;
+ }
+
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
@@ -1766,14 +1776,11 @@ namespace MediaBrowser.Controller.Entities
int curLen = current.Length;
if (curLen == 0)
{
- Studios = new[] { name };
+ Studios = [name];
}
else
{
- var newArr = new string[curLen + 1];
- current.CopyTo(newArr, 0);
- newArr[curLen] = name;
- Studios = newArr;
+ Studios = [..current, name];
}
}
}
@@ -1795,9 +1802,7 @@ namespace MediaBrowser.Controller.Entities
var genres = Genres;
if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
{
- var list = genres.ToList();
- list.Add(name);
- Genres = list.ToArray();
+ Genres = [..genres, name];
}
}
@@ -1944,14 +1949,15 @@ namespace MediaBrowser.Controller.Entities
return;
}
- // Remove it from the item
- RemoveImage(info);
-
+ // Remove from file system
if (info.IsLocalFile)
{
FileSystem.DeleteFile(info.Path);
}
+ // Remove from item
+ RemoveImage(info);
+
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
}
@@ -1967,12 +1973,7 @@ namespace MediaBrowser.Controller.Entities
public void AddImage(ItemImageInfo image)
{
- var current = ImageInfos;
- var currentCount = current.Length;
- var newArr = new ItemImageInfo[currentCount + 1];
- current.CopyTo(newArr, 0);
- newArr[currentCount] = image;
- ImageInfos = newArr;
+ ImageInfos = [..ImageInfos, image];
}
public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
@@ -2496,11 +2497,6 @@ namespace MediaBrowser.Controller.Entities
return new[] { Id };
}
- public virtual List<ExternalUrl> GetRelatedUrls()
- {
- return new List<ExternalUrl>();
- }
-
public virtual double? GetRefreshProgress()
{
return null;
@@ -2548,14 +2544,24 @@ namespace MediaBrowser.Controller.Entities
StringComparison.OrdinalIgnoreCase);
}
- public IReadOnlyList<BaseItem> GetThemeSongs()
+ public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
+ {
+ return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
+ }
+
+ public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> orderBy)
+ {
+ return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, orderBy).ToArray();
+ }
+
+ public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
{
- return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong).ToArray();
+ return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
}
- public IReadOnlyList<BaseItem> GetThemeVideos()
+ public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> orderBy)
{
- return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo).ToArray();
+ return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user, orderBy).ToArray();
}
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 676a47c88..4ead477f8 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -316,11 +316,12 @@ namespace MediaBrowser.Controller.Entities
/// <param name="progress">The progress.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
+ /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs
index 3005bee0a..c56603a3e 100644
--- a/MediaBrowser.Controller/Entities/Extensions.cs
+++ b/MediaBrowser.Controller/Entities/Extensions.cs
@@ -30,15 +30,11 @@ namespace MediaBrowser.Controller.Entities
if (item.RemoteTrailers.Count == 0)
{
- item.RemoteTrailers = new[] { mediaUrl };
+ item.RemoteTrailers = [mediaUrl];
}
else
{
- var oldIds = item.RemoteTrailers;
- var newIds = new MediaUrl[oldIds.Count + 1];
- oldIds.CopyTo(newIds);
- newIds[oldIds.Count] = mediaUrl;
- item.RemoteTrailers = newIds;
+ item.RemoteTrailers = [..item.RemoteTrailers, mediaUrl];
}
}
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index a2957cdca..b2e5d7263 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Security;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -269,11 +270,12 @@ namespace MediaBrowser.Controller.Entities
/// <param name="progress">The progress.</param>
/// <param name="metadataRefreshOptions">The metadata refresh options.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, CancellationToken cancellationToken = default)
+ public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, bool allowRemoveRoot = false, CancellationToken cancellationToken = default)
{
- return ValidateChildrenInternal(progress, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
+ return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
}
private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
@@ -307,11 +309,12 @@ namespace MediaBrowser.Controller.Entities
/// <param name="progress">The progress.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
+ /// <param name="allowRemoveRoot">remove item even this folder is root.</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
+ protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (recursive)
{
@@ -320,7 +323,7 @@ namespace MediaBrowser.Controller.Entities
try
{
- await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
+ await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
}
finally
{
@@ -331,8 +334,13 @@ namespace MediaBrowser.Controller.Entities
}
}
- private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item)
+ private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item, bool checkCollection)
{
+ if (!checkCollection && (item is BoxSet || string.Equals(item.FileNameWithoutExtension, "collections", StringComparison.OrdinalIgnoreCase)))
+ {
+ return true;
+ }
+
// For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
{
@@ -343,9 +351,9 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
+ private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
- if (!IsLibraryFolderAccessible(directoryService, this))
+ if (!IsLibraryFolderAccessible(directoryService, this, allowRemoveRoot))
{
return;
}
@@ -357,15 +365,23 @@ namespace MediaBrowser.Controller.Entities
if (IsFileProtocol)
{
- IEnumerable<BaseItem> nonCachedChildren;
+ IEnumerable<BaseItem> nonCachedChildren = [];
try
{
nonCachedChildren = GetNonCachedChildren(directoryService);
}
+ catch (IOException ex)
+ {
+ Logger.LogError(ex, "Error retrieving children from file system");
+ }
+ catch (SecurityException ex)
+ {
+ Logger.LogError(ex, "Error retrieving children from file system");
+ }
catch (Exception ex)
{
- Logger.LogError(ex, "Error retrieving children folder");
+ Logger.LogError(ex, "Error retrieving children");
return;
}
@@ -386,7 +402,7 @@ namespace MediaBrowser.Controller.Entities
foreach (var child in nonCachedChildren)
{
- if (!IsLibraryFolderAccessible(directoryService, child))
+ if (!IsLibraryFolderAccessible(directoryService, child, allowRemoveRoot))
{
continue;
}
@@ -414,12 +430,12 @@ namespace MediaBrowser.Controller.Entities
validChildren.Add(child);
}
+ // That's all the new and changed ones - now see if any have been removed and need cleanup
+ var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
+ var shouldRemove = !IsRoot || allowRemoveRoot;
// If it's an AggregateFolder, don't remove
- if (!IsRoot && currentChildren.Count != validChildren.Count)
+ if (shouldRemove && itemsRemoved.Count > 0)
{
- // That's all the new and changed ones - now see if there are any that are missing
- var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
-
foreach (var item in itemsRemoved)
{
if (item.IsFileProtocol)
@@ -460,15 +476,7 @@ namespace MediaBrowser.Controller.Entities
progress.Report(percent);
- // TODO: this is sometimes being called after the refresh has completed.
- try
- {
- ProviderManager.OnRefreshProgress(folder, percent);
- }
- catch (InvalidOperationException e)
- {
- Logger.LogError(e, "Error refreshing folder");
- }
+ ProviderManager.OnRefreshProgress(folder, percent);
});
if (validChildrenNeedGeneration)
@@ -500,15 +508,7 @@ namespace MediaBrowser.Controller.Entities
if (recursive)
{
- // TODO: this is sometimes being called after the refresh has completed.
- try
- {
- ProviderManager.OnRefreshProgress(folder, percent);
- }
- catch (InvalidOperationException e)
- {
- Logger.LogError(e, "Error refreshing folder");
- }
+ ProviderManager.OnRefreshProgress(folder, percent);
}
});
@@ -578,7 +578,7 @@ namespace MediaBrowser.Controller.Entities
private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
{
return RunTasks(
- (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, null, directoryService, cancellationToken),
+ (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, directoryService, cancellationToken),
children,
progress,
cancellationToken);
@@ -603,7 +603,7 @@ namespace MediaBrowser.Controller.Entities
}
var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
- var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : 2 * Environment.ProcessorCount;
+ var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount;
var actionBlock = new ActionBlock<int>(
async i =>
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 555dd050c..1461a3680 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -51,6 +51,7 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>();
VideoTypes = Array.Empty<VideoType>();
Years = Array.Empty<int>();
+ SkipDeserialization = false;
}
public InternalItemsQuery(User? user)
@@ -358,6 +359,8 @@ namespace MediaBrowser.Controller.Entities
public string? SeriesTimerId { get; set; }
+ public bool SkipDeserialization { get; set; }
+
public void SetUser(User user)
{
MaxParentalRating = user.MaxParentalAgeRating;
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index 81f6248fa..710b05e7f 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -45,9 +45,6 @@ namespace MediaBrowser.Controller.Entities.Movies
set => TmdbCollectionName = value;
}
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
-
public override double GetDefaultPrimaryImageAspectRatio()
{
// hack for tv plugins
@@ -124,23 +121,5 @@ namespace MediaBrowser.Controller.Entities.Movies
return hasChanges;
}
-
- /// <inheritdoc />
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId)
- });
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 37e241414..5c54f014c 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -344,22 +344,5 @@ namespace MediaBrowser.Controller.Entities.TV
return hasChanges;
}
-
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/episodes/{0}", imdbId)
- });
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index c29cefc15..083f12746 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -159,7 +159,7 @@ namespace MediaBrowser.Controller.Entities.TV
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
- var items = GetEpisodes(user, query.DtoOptions).Where(filter);
+ var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
return PostFilterAndSort(items, query, false);
}
@@ -169,30 +169,31 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
/// <param name="user">The user.</param>
/// <param name="options">The options to use.</param>
+ /// <param name="shouldIncludeMissingEpisodes">If missing episodes should be included.</param>
/// <returns>Set of episodes.</returns>
- public List<BaseItem> GetEpisodes(User user, DtoOptions options)
+ public List<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
- return GetEpisodes(Series, user, options);
+ return GetEpisodes(Series, user, options, shouldIncludeMissingEpisodes);
}
- public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options)
+ public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
- return GetEpisodes(series, user, null, options);
+ return GetEpisodes(series, user, null, options, shouldIncludeMissingEpisodes);
}
- public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options)
+ public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
- return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options);
+ return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
}
public List<BaseItem> GetEpisodes()
{
- return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true));
+ return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
}
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
- return GetEpisodes(user, new DtoOptions(true));
+ return GetEpisodes(user, new DtoOptions(true), true);
}
protected override bool GetBlockUnratedValue(User user)
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index a49c1609d..a324f79ef 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -28,7 +28,6 @@ namespace MediaBrowser.Controller.Entities.TV
public Series()
{
AirDays = Array.Empty<DayOfWeek>();
- SeasonNames = new Dictionary<int, string>();
}
public DayOfWeek[] AirDays { get; set; }
@@ -36,9 +35,6 @@ namespace MediaBrowser.Controller.Entities.TV
public string AirTime { get; set; }
[JsonIgnore]
- public Dictionary<int, string> SeasonNames { get; set; }
-
- [JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
[JsonIgnore]
@@ -73,9 +69,6 @@ namespace MediaBrowser.Controller.Entities.TV
/// <value>The status.</value>
public SeriesStatus? Status { get; set; }
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
-
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 2;
@@ -257,7 +250,7 @@ namespace MediaBrowser.Controller.Entities.TV
return LibraryManager.GetItemsResult(query);
}
- public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options)
+ public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
var seriesKey = GetUniqueSeriesKey(this);
@@ -267,10 +260,10 @@ namespace MediaBrowser.Controller.Entities.TV
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
- DtoOptions = options
+ DtoOptions = options,
};
- if (user is null || !user.DisplayMissingEpisodes)
+ if (!shouldIncludeMissingEpisodes)
{
query.IsMissing = false;
}
@@ -280,7 +273,7 @@ namespace MediaBrowser.Controller.Entities.TV
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
var allEpisodes = allItems.OfType<Season>()
- .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options))
+ .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes))
.Reverse();
// Specials could appear twice based on above - once in season 0, once in the aired season
@@ -292,8 +285,7 @@ namespace MediaBrowser.Controller.Entities.TV
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
- // Refresh bottom up, children first, then the boxset
- // By then hopefully the movies within will have Tmdb collection values
+ // Refresh bottom up, seasons and episodes first, then the series
var items = GetRecursiveChildren();
var totalItems = items.Count;
@@ -356,7 +348,7 @@ namespace MediaBrowser.Controller.Entities.TV
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
}
- public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options)
+ public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
@@ -373,24 +365,22 @@ namespace MediaBrowser.Controller.Entities.TV
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
- if (user is not null)
+
+ if (!shouldIncludeMissingEpisodes)
{
- if (!user.DisplayMissingEpisodes)
- {
- query.IsMissing = false;
- }
+ query.IsMissing = false;
}
var allItems = LibraryManager.GetItemList(query);
- return GetSeasonEpisodes(parentSeason, user, allItems, options);
+ return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
}
- public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options)
+ public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
if (allSeriesEpisodes is null)
{
- return GetSeasonEpisodes(parentSeason, user, options);
+ return GetSeasonEpisodes(parentSeason, user, options, shouldIncludeMissingEpisodes);
}
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
@@ -499,22 +489,5 @@ namespace MediaBrowser.Controller.Entities.TV
return hasChanges;
}
-
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/shows/{0}", imdbId)
- });
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs
index ec3eb0f70..c1e4d1db2 100644
--- a/MediaBrowser.Controller/Entities/TagExtensions.cs
+++ b/MediaBrowser.Controller/Entities/TagExtensions.cs
@@ -21,11 +21,11 @@ namespace MediaBrowser.Controller.Entities
{
if (current.Length == 0)
{
- item.Tags = new[] { name };
+ item.Tags = [name];
}
else
{
- item.Tags = current.Concat(new[] { name }).ToArray();
+ item.Tags = [..current, name];
}
}
}
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 1c558d419..939709215 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -23,9 +23,6 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>();
}
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
-
public TrailerType[] TrailerTypes { get; set; }
public override double GetDefaultPrimaryImageAspectRatio()
@@ -83,22 +80,5 @@ namespace MediaBrowser.Controller.Entities
return hasChanges;
}
-
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId)
- });
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index 69743b926..fc8a29763 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -117,11 +117,11 @@ namespace MediaBrowser.Controller.Entities
return base.GetNonCachedChildren(directoryService);
}
- protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
ClearCache();
- await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken)
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken)
.ConfigureAwait(false);
ClearCache();
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index c93488a85..e4fb340f7 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -6,10 +6,12 @@ using System;
using System.Collections.Generic;
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.Extensions;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Querying;
@@ -180,7 +182,7 @@ namespace MediaBrowser.Controller.Entities
return _originalFolderViewTypes.Contains(viewType);
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 4af000557..3a1d0c070 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -744,7 +744,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeSong.Value;
- var themeCount = item.GetThemeSongs().Count;
+ var themeCount = item.GetThemeSongs(user).Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)
@@ -757,7 +757,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeVideo.Value;
- var themeCount = item.GetThemeVideos().Count;
+ var themeCount = item.GetThemeVideos(user).Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index 6c58064ce..7dfda73bf 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -65,6 +65,11 @@ namespace MediaBrowser.Controller.Extensions
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
/// <summary>
+ /// Disable second level cache of sqlite.
+ /// </summary>
+ public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
+
+ /// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to retrieve the value from.</param>
@@ -128,5 +133,15 @@ namespace MediaBrowser.Controller.Extensions
/// <returns>The sqlite cache size.</returns>
public static int? GetSqliteCacheSize(this IConfiguration configuration)
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
+
+ /// <summary>
+ /// Gets whether second level cache disabled from the <see cref="IConfiguration" />.
+ /// </summary>
+ /// <param name="configuration">The configuration to read the setting from.</param>
+ /// <returns>Whether second level cache disabled.</returns>
+ public static bool GetSqliteSecondLevelCacheDisabled(this IConfiguration configuration)
+ {
+ return configuration.GetValue<bool>(SqliteDisableSecondLevelCacheKey);
+ }
}
}
diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs
new file mode 100644
index 000000000..1a33c3aa8
--- /dev/null
+++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs
@@ -0,0 +1,64 @@
+using System;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.IO;
+
+/// <summary>
+/// Helper methods for file system management.
+/// </summary>
+public static class FileSystemHelper
+{
+ /// <summary>
+ /// Deletes the file.
+ /// </summary>
+ /// <param name="fileSystem">The fileSystem.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="logger">The logger.</param>
+ public static void DeleteFile(IFileSystem fileSystem, string path, ILogger logger)
+ {
+ try
+ {
+ fileSystem.DeleteFile(path);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ logger.LogError(ex, "Error deleting file {Path}", path);
+ }
+ catch (IOException ex)
+ {
+ logger.LogError(ex, "Error deleting file {Path}", path);
+ }
+ }
+
+ /// <summary>
+ /// Recursively delete empty folders.
+ /// </summary>
+ /// <param name="fileSystem">The fileSystem.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="logger">The logger.</param>
+ public static void DeleteEmptyFolders(IFileSystem fileSystem, string path, ILogger logger)
+ {
+ foreach (var directory in fileSystem.GetDirectoryPaths(path))
+ {
+ DeleteEmptyFolders(fileSystem, directory, logger);
+ if (!fileSystem.GetFileSystemEntryPaths(directory).Any())
+ {
+ try
+ {
+ Directory.Delete(directory, false);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ logger.LogError(ex, "Error deleting directory {Path}", directory);
+ }
+ catch (IOException ex)
+ {
+ logger.LogError(ex, "Error deleting directory {Path}", directory);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 6532f7a34..b802b7e6e 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CA1002, CS1591
using System;
@@ -33,17 +31,17 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Occurs when [item added].
/// </summary>
- event EventHandler<ItemChangeEventArgs> ItemAdded;
+ event EventHandler<ItemChangeEventArgs>? ItemAdded;
/// <summary>
/// Occurs when [item updated].
/// </summary>
- event EventHandler<ItemChangeEventArgs> ItemUpdated;
+ event EventHandler<ItemChangeEventArgs>? ItemUpdated;
/// <summary>
/// Occurs when [item removed].
/// </summary>
- event EventHandler<ItemChangeEventArgs> ItemRemoved;
+ event EventHandler<ItemChangeEventArgs>? ItemRemoved;
/// <summary>
/// Gets the root folder.
@@ -60,10 +58,10 @@ namespace MediaBrowser.Controller.Library
/// <param name="parent">The parent.</param>
/// <param name="directoryService">An instance of <see cref="IDirectoryService"/>.</param>
/// <returns>BaseItem.</returns>
- BaseItem ResolvePath(
+ BaseItem? ResolvePath(
FileSystemMetadata fileInfo,
- Folder parent = null,
- IDirectoryService directoryService = null);
+ Folder? parent = null,
+ IDirectoryService? directoryService = null);
/// <summary>
/// Resolves a set of files into a list of BaseItem.
@@ -86,7 +84,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="name">The name of the person.</param>
/// <returns>Task{Person}.</returns>
- Person GetPerson(string name);
+ Person? GetPerson(string name);
/// <summary>
/// Finds the by path.
@@ -94,7 +92,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="path">The path.</param>
/// <param name="isFolder"><c>true</c> is the path is a directory; otherwise <c>false</c>.</param>
/// <returns>BaseItem.</returns>
- BaseItem FindByPath(string path, bool? isFolder);
+ BaseItem? FindByPath(string path, bool? isFolder);
/// <summary>
/// Gets the artist.
@@ -151,6 +149,14 @@ namespace MediaBrowser.Controller.Library
/// <returns>Task.</returns>
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
+ /// <summary>
+ /// Reloads the root media folder.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="removeRoot">Is remove the library itself allowed.</param>
+ /// <returns>Task.</returns>
+ Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false);
+
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
/// <summary>
@@ -166,7 +172,8 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
- BaseItem GetItemById(Guid id);
+ /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
+ BaseItem? GetItemById(Guid id);
/// <summary>
/// Gets the item by id, as T.
@@ -174,7 +181,27 @@ namespace MediaBrowser.Controller.Library
/// <param name="id">The item id.</param>
/// <typeparam name="T">The type of item.</typeparam>
/// <returns>The item.</returns>
- T GetItemById<T>(Guid id)
+ T? GetItemById<T>(Guid id)
+ where T : BaseItem;
+
+ /// <summary>
+ /// Gets the item by id, as T, and validates user access.
+ /// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="userId">The user id to validate against.</param>
+ /// <typeparam name="T">The type of item.</typeparam>
+ /// <returns>The item if found.</returns>
+ public T? GetItemById<T>(Guid id, Guid userId)
+ where T : BaseItem;
+
+ /// <summary>
+ /// Gets the item by id, as T, and validates user access.
+ /// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="user">The user to validate against.</param>
+ /// <typeparam name="T">The type of item.</typeparam>
+ /// <returns>The item if found.</returns>
+ public T? GetItemById<T>(Guid id, User? user)
where T : BaseItem;
/// <summary>
@@ -208,9 +235,9 @@ namespace MediaBrowser.Controller.Library
/// <param name="sortBy">The sort by.</param>
/// <param name="sortOrder">The sort order.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
- IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder);
+ IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder);
- IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy);
+ IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy);
/// <summary>
/// Gets the user root folder.
@@ -223,7 +250,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="item">Item to create.</param>
/// <param name="parent">Parent of new item.</param>
- void CreateItem(BaseItem item, BaseItem parent);
+ void CreateItem(BaseItem item, BaseItem? parent);
/// <summary>
/// Creates the items.
@@ -231,7 +258,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="items">Items to create.</param>
/// <param name="parent">Parent of new items.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
- void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken);
+ void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken);
/// <summary>
/// Updates the item.
@@ -509,7 +536,7 @@ namespace MediaBrowser.Controller.Library
/// <returns>QueryResult&lt;BaseItem&gt;.</returns>
QueryResult<BaseItem> QueryItems(InternalItemsQuery query);
- string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem = null);
+ string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem = null);
/// <summary>
/// Converts the image to local.
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index bace703ad..44a1a85e3 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -138,7 +138,7 @@ namespace MediaBrowser.Controller.Library
MediaProtocol GetPathProtocol(string path);
- void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user);
+ void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user);
Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken);
}
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index 6202f92f5..b558ef73d 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -116,8 +116,8 @@ namespace MediaBrowser.Controller.Library
{
get
{
- var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path };
- return AdditionalLocations is null ? paths : paths.Concat(AdditionalLocations).ToArray();
+ var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : [Path];
+ return AdditionalLocations is null ? paths : [..paths, ..AdditionalLocations];
}
}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs
index 881c42c73..3a062a467 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs
@@ -9,10 +9,6 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
public class LiveTvConflictException : Exception
{
- public LiveTvConflictException()
- {
- }
-
public LiveTvConflictException(string message)
: base(message)
{
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index 05540d490..2ac6f9963 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -254,25 +254,5 @@ namespace MediaBrowser.Controller.LiveTv
return name;
}
-
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- if (IsMovie)
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId)
- });
- }
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index f237993fd..1ef2eb343 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
- <VersionPrefix>10.9.0</VersionPrefix>
+ <VersionPrefix>10.10.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index 29dd190ab..03ec6c658 100644
--- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -191,6 +191,8 @@ namespace MediaBrowser.Controller.MediaEncoding
public Dictionary<string, string> StreamOptions { get; set; }
+ public bool EnableAudioVbrEncoding { get; set; }
+
public string GetOption(string qualifier, string name)
{
var value = GetOption(qualifier + "-" + name);
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 5143d5f74..eb80bab2d 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@@ -55,6 +56,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minKerneli915Hang = new Version(5, 18);
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
+ private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
@@ -108,7 +110,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "wmav2", 2 },
{ "libmp3lame", 2 },
{ "libfdk_aac", 6 },
- { "aac_at", 6 },
{ "ac3", 6 },
{ "eac3", 6 },
{ "dca", 6 },
@@ -120,7 +121,8 @@ namespace MediaBrowser.Controller.MediaEncoding
private static readonly Dictionary<string, string> _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase)
{
{ "vaapi", _defaultMjpegEncoder + "_vaapi" },
- { "qsv", _defaultMjpegEncoder + "_qsv" }
+ { "qsv", _defaultMjpegEncoder + "_qsv" },
+ { "videotoolbox", _defaultMjpegEncoder + "_videotoolbox" }
};
public static readonly string[] LosslessAudioCodecs = new string[]
@@ -285,6 +287,21 @@ namespace MediaBrowser.Controller.MediaEncoding
// Let transpose_vt optional for the time being.
}
+ private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
+ {
+ if (state.VideoStream is null
+ || !options.EnableTonemapping
+ || GetVideoColorBitDepth(state) != 10
+ || !_mediaEncoder.SupportsFilter("tonemapx")
+ || !(string.Equals(state.VideoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream?.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+
+ return state.VideoStream.VideoRange == VideoRange.HDR
+ && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG;
+ }
+
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream is null
@@ -691,16 +708,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return -1;
}
- public string GetInputPathArgument(EncodingJobInfo state)
- {
- return state.MediaSource.VideoType switch
- {
- VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource),
- VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource),
- _ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource)
- };
- }
-
/// <summary>
/// Gets the audio encoder.
/// </summary>
@@ -762,6 +769,15 @@ namespace MediaBrowser.Controller.MediaEncoding
return "dca";
}
+ if (string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ // The ffmpeg upstream breaks the AudioToolbox ALAC encoder in version 6.1 but fixes it in version 7.0.
+ // Since ALAC is lossless in quality and the AudioToolbox encoder is not faster,
+ // its only benefit is a smaller file size.
+ // To prevent problems, use the ffmpeg native encoder instead.
+ return "alac";
+ }
+
return codec.ToLowerInvariant();
}
@@ -1007,7 +1023,8 @@ namespace MediaBrowser.Controller.MediaEncoding
Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
if (IsVulkanFullSupported()
- && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
+ && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
+ && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
@@ -1194,15 +1211,20 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
{
- var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
- _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
- arg.Append(" -f concat -safe 0 -i ")
- .Append(tmpConcatPath);
+ var concatFilePath = Path.Join(_configurationManager.CommonApplicationPaths.CachePath, "concat", state.MediaSource.Id + ".concat");
+ if (!File.Exists(concatFilePath))
+ {
+ _mediaEncoder.GenerateConcatConfig(state.MediaSource, concatFilePath);
+ }
+
+ arg.Append(" -f concat -safe 0 -i \"")
+ .Append(concatFilePath)
+ .Append("\" ");
}
else
{
arg.Append(" -i ")
- .Append(GetInputPathArgument(state));
+ .Append(_mediaEncoder.GetInputPathArgument(state));
}
// sub2video for external graphical subtitles
@@ -1214,8 +1236,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var subtitlePath = state.SubtitleStream.Path;
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
- if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
- || subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
+ // dvdsub/vobsub graphical subtitles use .sub+.idx pairs
+ if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
{
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
if (File.Exists(idxFile))
@@ -1270,23 +1292,23 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = stream.Codec ?? string.Empty;
- return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1
- || codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
+ return codec.Contains("264", StringComparison.OrdinalIgnoreCase)
+ || codec.Contains("avc", StringComparison.OrdinalIgnoreCase);
}
public static bool IsH265(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
- return codec.IndexOf("265", StringComparison.OrdinalIgnoreCase) != -1
- || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
+ return codec.Contains("265", StringComparison.OrdinalIgnoreCase)
+ || codec.Contains("hevc", StringComparison.OrdinalIgnoreCase);
}
public static bool IsAAC(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
- return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1;
+ return codec.Contains("aac", StringComparison.OrdinalIgnoreCase);
}
public static string GetBitStreamArgs(MediaStream stream)
@@ -1340,7 +1362,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return ".ts";
}
- public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
+ private string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{
if (state.OutputVideoBitrate is null)
{
@@ -1409,7 +1431,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// The `maxrate` and `bufsize` options can potentially lead to performance regression
// and even encoder hangs, especially when the value is very high.
- return FormattableString.Invariant($" -b:v {bitrate}");
+ return FormattableString.Invariant($" -b:v {bitrate} -qmin -1 -qmax -1");
}
return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
@@ -2082,6 +2104,18 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "constrained_high";
}
+ if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "constrained_baseline";
+ }
+
+ if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("constrainedhigh", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "constrained_high";
+ }
+
if (!string.IsNullOrEmpty(profile))
{
// Currently there's no profile option in av1_nvenc encoder
@@ -2315,7 +2349,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (request.VideoBitRate.HasValue
&& (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value))
{
- return false;
+ // For LiveTV that has no bitrate, let's try copy if other conditions are met
+ if (string.IsNullOrWhiteSpace(request.LiveStreamId) || videoStream.BitRate.HasValue)
+ {
+ return false;
+ }
}
var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec);
@@ -2575,8 +2613,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 2);
}
- public string GetAudioVbrModeParam(string encoder, int bitratePerChannel)
+ public string GetAudioVbrModeParam(string encoder, int bitrate, int channels)
{
+ var bitratePerChannel = bitrate / Math.Max(channels, 1);
if (string.Equals(encoder, "libfdk_aac", StringComparison.OrdinalIgnoreCase))
{
return " -vbr:a " + bitratePerChannel switch
@@ -2591,14 +2630,26 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(encoder, "libmp3lame", StringComparison.OrdinalIgnoreCase))
{
- return " -qscale:a " + bitratePerChannel switch
+ // lame's VBR is only good for a certain bitrate range
+ // For very low and very high bitrate, use abr mode
+ if (bitratePerChannel is < 122500 and > 48000)
{
- < 48000 => "8",
- < 64000 => "6",
- < 88000 => "4",
- < 112000 => "2",
- _ => "0"
- };
+ return " -qscale:a " + bitratePerChannel switch
+ {
+ < 64000 => "6",
+ < 88000 => "4",
+ < 112000 => "2",
+ _ => "0"
+ };
+ }
+
+ return " -abr:a 1" + " -b:a " + bitrate;
+ }
+
+ if (string.Equals(encoder, "aac_at", StringComparison.OrdinalIgnoreCase))
+ {
+ // aac_at's CVBR mode
+ return " -aac_at_mode:a 2" + " -b:a " + bitrate;
}
if (string.Equals(encoder, "libvorbis", StringComparison.OrdinalIgnoreCase))
@@ -2626,12 +2677,16 @@ namespace MediaBrowser.Controller.MediaEncoding
&& channels.Value == 2
&& state.AudioStream is not null
&& state.AudioStream.Channels.HasValue
- && state.AudioStream.Channels.Value > 5)
+ && state.AudioStream.Channels.Value == 6)
{
+ if (!encodingOptions.DownMixAudioBoost.Equals(1))
+ {
+ filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
+ }
+
switch (encodingOptions.DownMixStereoAlgorithm)
{
case DownMixStereoAlgorithms.Dave750:
- filters.Add("volume=4.25");
filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
break;
case DownMixStereoAlgorithms.NightmodeDialogue:
@@ -2639,11 +2694,6 @@ namespace MediaBrowser.Controller.MediaEncoding
break;
case DownMixStereoAlgorithms.None:
default:
- if (!encodingOptions.DownMixAudioBoost.Equals(1))
- {
- filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
- }
-
break;
}
}
@@ -2719,7 +2769,20 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.TranscodingType != TranscodingJobType.Progressive
&& ((resultChannels > 2 && resultChannels < 6) || resultChannels == 7))
{
- resultChannels = 2;
+ // We can let FFMpeg supply an extra LFE channel for 5ch and 7ch to make them 5.1 and 7.1
+ if (resultChannels == 5)
+ {
+ resultChannels = 6;
+ }
+ else if (resultChannels == 7)
+ {
+ resultChannels = 8;
+ }
+ else
+ {
+ // For other weird layout, just downmix to stereo for compatibility
+ resultChannels = 2;
+ }
}
}
@@ -2757,7 +2820,13 @@ namespace MediaBrowser.Controller.MediaEncoding
if (time > 0)
{
- seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(time));
+ // For direct streaming/remuxing, we seek at the exact position of the keyframe
+ // However, ffmpeg will seek to previous keyframe when the exact time is the input
+ // Workaround this by adding 0.5s offset to the seeking time to get the exact keyframe on most videos.
+ // This will help subtitle syncing.
+ var isHlsRemuxing = state.IsVideoRequest && state.TranscodingType is TranscodingJobType.Hls && IsCopyCodec(state.OutputVideoCodec);
+ var seekTick = isHlsRemuxing ? time + 5000000L : time;
+ seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick));
if (state.IsVideoRequest)
{
@@ -2970,8 +3039,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var scaleW = (double)maximumWidth / outputWidth;
var scaleH = (double)maximumHeight / outputHeight;
var scale = Math.Min(scaleW, scaleH);
- outputWidth = Math.Min(maximumWidth, (int)(outputWidth * scale));
- outputHeight = Math.Min(maximumHeight, (int)(outputHeight * scale));
+ outputWidth = Math.Min(maximumWidth, Convert.ToInt32(outputWidth * scale));
+ outputHeight = Math.Min(maximumHeight, Convert.ToInt32(outputHeight * scale));
}
outputWidth = 2 * (outputWidth / 2);
@@ -3147,7 +3216,9 @@ namespace MediaBrowser.Controller.MediaEncoding
int? requestedMaxHeight)
{
var isV4l2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
+ var isMjpeg = videoEncoder is not null && videoEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var scaleVal = isV4l2 ? 64 : 2;
+ var targetAr = isMjpeg ? "(a*sar)" : "a"; // manually calculate AR when using mjpeg encoder
// If fixed dimensions were supplied
if (requestedWidth.HasValue && requestedHeight.HasValue)
@@ -3176,10 +3247,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- @"scale=trunc(min(max(iw\,ih*a)\,min({0}\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\,ih)\,min({0}/a\,{1}))/2)*2",
+ @"scale=trunc(min(max(iw\,ih*{3})\,min({0}\,{1}*{3}))/{2})*{2}:trunc(min(max(iw/{3}\,ih)\,min({0}/{3}\,{1}))/2)*2",
maxWidthParam,
maxHeightParam,
- scaleVal);
+ scaleVal,
+ targetAr);
}
// If a fixed width was requested
@@ -3195,8 +3267,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- "scale={0}:trunc(ow/a/2)*2",
- widthParam);
+ "scale={0}:trunc(ow/{1}/2)*2",
+ widthParam,
+ targetAr);
}
// If a fixed height was requested
@@ -3206,9 +3279,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- "scale=trunc(oh*a/{1})*{1}:{0}",
+ "scale=trunc(oh*{2}/{1})*{1}:{0}",
heightParam,
- scaleVal);
+ scaleVal,
+ targetAr);
}
// If a max width was requested
@@ -3218,9 +3292,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- @"scale=trunc(min(max(iw\,ih*a)\,{0})/{1})*{1}:trunc(ow/a/2)*2",
+ @"scale=trunc(min(max(iw\,ih*{2})\,{0})/{1})*{1}:trunc(ow/{2}/2)*2",
maxWidthParam,
- scaleVal);
+ scaleVal,
+ targetAr);
}
// If a max height was requested
@@ -3230,9 +3305,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- @"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\,ih)\,{0})",
+ @"scale=trunc(oh*{2}/{1})*{1}:min(max(iw/{2}\,ih)\,{0})",
maxHeightParam,
- scaleVal);
+ scaleVal,
+ targetAr);
}
return string.Empty;
@@ -3499,6 +3575,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doDeintH2645 = doDeintH264 || doDeintHevc;
+ var doToneMap = IsSwTonemapAvailable(state, options);
var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
@@ -3512,7 +3589,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/* Make main filters for video stream */
var mainFilters = new List<string>();
- mainFilters.Add(GetOverwriteColorPropertiesParam(state, false));
+ mainFilters.Add(GetOverwriteColorPropertiesParam(state, doToneMap));
// INPUT sw surface(memory/copy-back from vram)
// sw deint
@@ -3535,11 +3612,31 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw scale
mainFilters.Add(swScaleFilter);
- mainFilters.Add("format=" + outFormat);
- // sw tonemap <= TODO: finsh the fast tonemap filter
+ // sw tonemap <= TODO: finish dovi tone mapping
- // OUTPUT yuv420p/nv12 surface(memory)
+ if (doToneMap)
+ {
+ var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={outFormat}";
+
+ if (options.TonemappingParam != 0)
+ {
+ tonemapArgs += $":param={options.TonemappingParam}";
+ }
+
+ if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase))
+ {
+ tonemapArgs += $":range={options.TonemappingRange}";
+ }
+
+ mainFilters.Add(tonemapArgs);
+ }
+ else
+ {
+ // OUTPUT yuv420p/nv12 surface(memory)
+ mainFilters.Add("format=" + outFormat);
+ }
/* Make sub and overlay filters for subtitle stream */
var subFilters = new List<string>();
@@ -4357,6 +4454,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// map from qsv to vaapi.
mainFilters.Add("hwmap=derive_device=vaapi");
+ mainFilters.Add("format=vaapi");
}
var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
@@ -4366,6 +4464,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// map from vaapi to qsv.
mainFilters.Add("hwmap=derive_device=qsv");
+ mainFilters.Add("format=qsv");
}
}
@@ -4540,7 +4639,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// prefered vaapi + vulkan filters pipeline
if (_mediaEncoder.IsVaapiDeviceAmd
&& isVaapiVkSupported
- && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
+ && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
+ && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{
// AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
@@ -5257,11 +5357,6 @@ namespace MediaBrowser.Controller.MediaEncoding
/* Make main filters for video stream */
var mainFilters = new List<string>();
- // INPUT videotoolbox/memory surface(vram/uma)
- // this will pass-through automatically if in/out format matches.
- mainFilters.Add("format=nv12|p010le|videotoolbox_vld");
- mainFilters.Add("hwupload=derive_device=videotoolbox");
-
// hw deint
if (doDeintH2645)
{
@@ -5323,6 +5418,21 @@ namespace MediaBrowser.Controller.MediaEncoding
overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0");
}
+ var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) ||
+ subFilters.Any(f => !string.IsNullOrEmpty(f)) ||
+ overlayFilters.Any(f => !string.IsNullOrEmpty(f));
+
+ // This is a workaround for ffmpeg's hwupload implementation
+ // For VideoToolbox encoders, a hwupload without a valid filter actually consuming its frame
+ // will cause the encoder to produce incorrect frames.
+ if (needFiltering)
+ {
+ // INPUT videotoolbox/memory surface(vram/uma)
+ // this will pass-through automatically if in/out format matches.
+ mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
+ mainFilters.Insert(0, "hwupload=derive_device=videotoolbox");
+ }
+
return (mainFilters, subFilters, overlayFilters);
}
@@ -5813,16 +5923,29 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var bitDepth = GetVideoColorBitDepth(state);
- // Only HEVC, VP9 and AV1 formats have 10-bit hardware decoder support now.
+ // Only HEVC, VP9 and AV1 formats have 10-bit hardware decoder support for most platforms
if (bitDepth == 10
&& !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)))
{
- // One exception is that RKMPP decoder can handle H.264 High 10.
- if (!(string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)
- && string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)))
+ // RKMPP has H.264 Hi10P decoder
+ bool hasHardwareHi10P = string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase);
+
+ // VideoToolbox on Apple Silicon has H.264 Hi10P mode enabled after macOS 14.6
+ if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ var ver = Environment.OSVersion.Version;
+ var arch = RuntimeInformation.OSArchitecture;
+ if (arch.Equals(Architecture.Arm64) && ver >= new Version(14, 6))
+ {
+ hasHardwareHi10P = true;
+ }
+ }
+
+ if (!hasHardwareHi10P
+ && string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -7049,7 +7172,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var channels = state.OutputAudioChannels;
- if (channels.HasValue && ((channels.Value != 2 && state.AudioStream.Channels <= 5) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))
+ if (channels.HasValue && ((channels.Value != 2 && state.AudioStream?.Channels != 6) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))
{
args += " -ac " + channels.Value;
}
@@ -7057,8 +7180,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var bitrate = state.OutputAudioBitrate;
if (bitrate.HasValue && !LosslessAudioCodecs.Contains(codec, StringComparison.OrdinalIgnoreCase))
{
- var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / (channels ?? 2));
- if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value, channels ?? 2);
+ if (encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null)
{
args += vbrParam;
}
@@ -7088,8 +7211,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase))
{
- var vbrParam = GetAudioVbrModeParam(GetAudioEncoder(state), bitrate.Value / (channels ?? 2));
- if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ var vbrParam = GetAudioVbrModeParam(GetAudioEncoder(state), bitrate.Value, channels ?? 2);
+ if (encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null)
{
audioTranscodeParams.Add(vbrParam);
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index f2a0b906d..72df7151d 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -508,6 +508,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ public bool EnableAudioVbrEncoding => BaseRequest.EnableAudioVbrEncoding;
+
public int HlsListSize => 0;
public bool EnableBreakOnNonKeyFrames(string videoCodec)
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index e696fa52c..038c6c7f6 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -153,6 +153,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="threads">The input/output thread count for ffmpeg.</param>
/// <param name="qualityScale">The qscale value for ffmpeg.</param>
/// <param name="priority">The process priority for the ffmpeg process.</param>
+ /// <param name="enableKeyFrameOnlyExtraction">Whether to only extract key frames.</param>
/// <param name="encodingHelper">EncodingHelper instance.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Directory where images where extracted. A given image made before another will always be named with a lower number.</returns>
@@ -168,6 +169,7 @@ namespace MediaBrowser.Controller.MediaEncoding
int? threads,
int? qualityScale,
ProcessPriorityClass? priority,
+ bool enableKeyFrameOnlyExtraction,
EncodingHelper encodingHelper,
CancellationToken cancellationToken);
@@ -246,6 +248,21 @@ namespace MediaBrowser.Controller.MediaEncoding
IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
/// <summary>
+ /// Gets the input path argument from <see cref="EncodingJobInfo"/>.
+ /// </summary>
+ /// <param name="state">The <see cref="EncodingJobInfo"/>.</param>
+ /// <returns>The input path argument.</returns>
+ string GetInputPathArgument(EncodingJobInfo state);
+
+ /// <summary>
+ /// Gets the input path argument.
+ /// </summary>
+ /// <param name="path">The item path.</param>
+ /// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
+ /// <returns>The input path argument.</returns>
+ string GetInputPathArgument(string path, MediaSourceInfo mediaSource);
+
+ /// <summary>
/// Generates a FFmpeg concat config for the source.
/// </summary>
/// <param name="source">The <see cref="MediaSourceInfo"/>.</param>
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 219da309e..a47d2fa45 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.Net
SingleWriter = false
});
- private readonly SemaphoreSlim _lock = new(1, 1);
+ private readonly object _activeConnectionsLock = new();
/// <summary>
/// The _active connections.
@@ -126,15 +126,10 @@ namespace MediaBrowser.Controller.Net
InitialDelayMs = dueTimeMs
};
- _lock.Wait();
- try
+ lock (_activeConnectionsLock)
{
_activeConnections.Add((message.Connection, cancellationTokenSource, state));
}
- finally
- {
- _lock.Release();
- }
}
protected void SendData(bool force)
@@ -153,8 +148,7 @@ namespace MediaBrowser.Controller.Net
(IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)[] tuples;
var now = DateTime.UtcNow;
- await _lock.WaitAsync().ConfigureAwait(false);
- try
+ lock (_activeConnectionsLock)
{
if (_activeConnections.Count == 0)
{
@@ -174,10 +168,6 @@ namespace MediaBrowser.Controller.Net
})
.ToArray();
}
- finally
- {
- _lock.Release();
- }
if (tuples.Length == 0)
{
@@ -240,8 +230,7 @@ namespace MediaBrowser.Controller.Net
/// <param name="message">The message.</param>
private void Stop(WebSocketMessageInfo message)
{
- _lock.Wait();
- try
+ lock (_activeConnectionsLock)
{
var connection = _activeConnections.FirstOrDefault(c => c.Connection == message.Connection);
@@ -250,10 +239,6 @@ namespace MediaBrowser.Controller.Net
DisposeConnection(connection);
}
}
- finally
- {
- _lock.Release();
- }
}
/// <summary>
@@ -283,15 +268,10 @@ namespace MediaBrowser.Controller.Net
Logger.LogError(ex, "Error disposing websocket");
}
- _lock.Wait();
- try
+ lock (_activeConnectionsLock)
{
_activeConnections.Remove(connection);
}
- finally
- {
- _lock.Release();
- }
}
protected virtual async ValueTask DisposeAsyncCore()
@@ -306,18 +286,13 @@ namespace MediaBrowser.Controller.Net
Logger.LogError(ex, "Disposing the message consumer failed");
}
- await _lock.WaitAsync().ConfigureAwait(false);
- try
+ lock (_activeConnectionsLock)
{
- foreach (var connection in _activeConnections.ToArray())
+ foreach (var connection in _activeConnections.ToList())
{
DisposeConnection(connection);
}
}
- finally
- {
- _lock.Release();
- }
}
/// <inheritdoc />
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index bb68a3b6d..038cbd2d6 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Playlists;
namespace MediaBrowser.Controller.Playlists
@@ -11,18 +12,49 @@ namespace MediaBrowser.Controller.Playlists
public interface IPlaylistManager
{
/// <summary>
- /// Gets the playlists.
+ /// Gets the playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>Playlist.</returns>
+ Playlist GetPlaylistForUser(Guid playlistId, Guid userId);
+
+ /// <summary>
+ /// Creates the playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistCreationRequest"/>.</param>
+ /// <returns>The created playlist.</returns>
+ Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest request);
+
+ /// <summary>
+ /// Updates a playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistUpdateRequest"/>.</param>
+ /// <returns>Task.</returns>
+ Task UpdatePlaylist(PlaylistUpdateRequest request);
+
+ /// <summary>
+ /// Gets all playlists a user has access to.
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>IEnumerable&lt;Playlist&gt;.</returns>
IEnumerable<Playlist> GetPlaylists(Guid userId);
/// <summary>
- /// Creates the playlist.
+ /// Adds a share to the playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistUserUpdateRequest"/>.</param>
+ /// <returns>Task.</returns>
+ Task AddUserToShares(PlaylistUserUpdateRequest request);
+
+ /// <summary>
+ /// Removes a share from the playlist.
/// </summary>
- /// <param name="options">The options.</param>
- /// <returns>Task&lt;Playlist&gt;.</returns>
- Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options);
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ /// <param name="share">The share.</param>
+ /// <returns>Task.</returns>
+ Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share);
/// <summary>
/// Adds to playlist.
@@ -31,7 +63,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="itemIds">The item ids.</param>
/// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns>
- Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
+ Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary>
/// Removes from playlist.
@@ -39,7 +71,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="playlistId">The playlist identifier.</param>
/// <param name="entryIds">The entry ids.</param>
/// <returns>Task.</returns>
- Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds);
+ Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds);
/// <summary>
/// Gets the playlists folder.
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index ca032e7f6..45aefacf6 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -16,24 +16,23 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Playlists
{
public class Playlist : Folder, IHasShares
{
- public static readonly IReadOnlyList<string> SupportedExtensions = new[]
- {
+ public static readonly IReadOnlyList<string> SupportedExtensions =
+ [
".m3u",
".m3u8",
".pls",
".wpl",
".zpl"
- };
+ ];
public Playlist()
{
- Shares = Array.Empty<Share>();
+ Shares = [];
OpenAccess = false;
}
@@ -41,7 +40,7 @@ namespace MediaBrowser.Controller.Playlists
public bool OpenAccess { get; set; }
- public Share[] Shares { get; set; }
+ public IReadOnlyList<PlaylistUserPermissions> Shares { get; set; }
[JsonIgnore]
public bool IsFile => IsPlaylistFile(Path);
@@ -130,10 +129,10 @@ namespace MediaBrowser.Controller.Playlists
protected override List<BaseItem> LoadChildren()
{
// Save a trip to the database
- return new List<BaseItem>();
+ return [];
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
@@ -145,7 +144,7 @@ namespace MediaBrowser.Controller.Playlists
protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
{
- return new List<BaseItem>();
+ return [];
}
public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
@@ -167,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
return base.GetChildren(user, true, query);
}
- public static List<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
+ public static IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
{
if (user is not null)
{
@@ -178,23 +177,23 @@ namespace MediaBrowser.Controller.Playlists
foreach (var item in inputItems)
{
- var playlistItems = GetPlaylistItems(item, user, playlistMediaType, options);
+ var playlistItems = GetPlaylistItems(item, user, options);
list.AddRange(playlistItems);
}
return list;
}
- private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options)
+ private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, DtoOptions options)
{
if (item is MusicGenre musicGenre)
{
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { BaseItemKind.Audio },
- GenreIds = new[] { musicGenre.Id },
- OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Audio],
+ GenreIds = [musicGenre.Id],
+ OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)],
DtoOptions = options
});
}
@@ -204,9 +203,9 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { BaseItemKind.Audio },
- ArtistIds = new[] { musicArtist.Id },
- OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Audio],
+ ArtistIds = [musicArtist.Id],
+ OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)],
DtoOptions = options
});
}
@@ -217,8 +216,7 @@ namespace MediaBrowser.Controller.Playlists
{
Recursive = true,
IsFolder = false,
- OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
- MediaTypes = new[] { mediaType },
+ MediaTypes = [MediaType.Audio, MediaType.Video],
EnableTotalRecordCount = false,
DtoOptions = options
};
@@ -226,7 +224,7 @@ namespace MediaBrowser.Controller.Playlists
return folder.GetItemList(query);
}
- return new[] { item };
+ return [item];
}
public override bool IsVisible(User user)
@@ -248,12 +246,17 @@ namespace MediaBrowser.Controller.Playlists
}
var shares = Shares;
- if (shares.Length == 0)
+ if (shares.Count == 0)
{
return false;
}
- return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId));
+ return shares.Any(s => s.UserId.Equals(userId));
+ }
+
+ public override bool CanDelete(User user)
+ {
+ return user.HasPermission(PermissionKind.IsAdministrator) || user.Id.Equals(OwnerUserId);
}
public override bool IsVisibleStandalone(User user)
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 7fe2f64af..474f09dc5 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -28,6 +28,22 @@ namespace MediaBrowser.Controller.Providers
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
}
+ public List<FileSystemMetadata> GetDirectories(string path)
+ {
+ var list = new List<FileSystemMetadata>();
+ var items = GetFileSystemEntries(path);
+ for (var i = 0; i < items.Length; i++)
+ {
+ var item = items[i];
+ if (item.IsDirectory)
+ {
+ list.Add(item);
+ }
+ }
+
+ return list;
+ }
+
public List<FileSystemMetadata> GetFiles(string path)
{
var list = new List<FileSystemMetadata>();
@@ -46,10 +62,22 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata? GetFile(string path)
{
+ var entry = GetFileSystemEntry(path);
+ return entry is not null && !entry.IsDirectory ? entry : null;
+ }
+
+ public FileSystemMetadata? GetDirectory(string path)
+ {
+ var entry = GetFileSystemEntry(path);
+ return entry is not null && entry.IsDirectory ? entry : null;
+ }
+
+ public FileSystemMetadata? GetFileSystemEntry(string path)
+ {
if (!_fileCache.TryGetValue(path, out var result))
{
- var file = _fileSystem.GetFileInfo(path);
- if (file.Exists)
+ var file = _fileSystem.GetFileSystemInfo(path);
+ if (file?.Exists ?? false)
{
result = file;
_fileCache.TryAdd(path, result);
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index 6d7550ab5..1babf73af 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -9,10 +9,16 @@ namespace MediaBrowser.Controller.Providers
{
FileSystemMetadata[] GetFileSystemEntries(string path);
+ List<FileSystemMetadata> GetDirectories(string path);
+
List<FileSystemMetadata> GetFiles(string path);
FileSystemMetadata? GetFile(string path);
+ FileSystemMetadata? GetDirectory(string path);
+
+ FileSystemMetadata? GetFileSystemEntry(string path);
+
IReadOnlyList<string> GetFilePaths(string path);
IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false);
diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs
index 0d847520d..f451eac6d 100644
--- a/MediaBrowser.Controller/Providers/IExternalId.cs
+++ b/MediaBrowser.Controller/Providers/IExternalId.cs
@@ -1,3 +1,4 @@
+using System;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -33,6 +34,7 @@ namespace MediaBrowser.Controller.Providers
/// <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>
diff --git a/MediaBrowser.Controller/Providers/IExternalUrlProvider.cs b/MediaBrowser.Controller/Providers/IExternalUrlProvider.cs
new file mode 100644
index 000000000..86a180627
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IExternalUrlProvider.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Providers;
+
+/// <summary>
+/// Interface to include related urls for an item.
+/// </summary>
+public interface IExternalUrlProvider
+{
+ /// <summary>
+ /// Gets the external service name.
+ /// </summary>
+ string Name { get; }
+
+ /// <summary>
+ /// Get the list of external urls.
+ /// </summary>
+ /// <param name="item">The item to get external urls for.</param>
+ /// <returns>The list of external urls.</returns>
+ IEnumerable<string> GetExternalUrls(BaseItem item);
+}
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index eb5069b06..38fc5f2cc 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -99,12 +99,14 @@ namespace MediaBrowser.Controller.Providers
/// <param name="metadataProviders">Metadata providers to use.</param>
/// <param name="metadataSavers">Metadata savers to use.</param>
/// <param name="externalIds">External IDs to use.</param>
+ /// <param name="externalUrlProviders">The list of external url providers.</param>
void AddParts(
IEnumerable<IImageProvider> imageProviders,
IEnumerable<IMetadataService> metadataServices,
IEnumerable<IMetadataProvider> metadataProviders,
IEnumerable<IMetadataSaver> metadataSavers,
- IEnumerable<IExternalId> externalIds);
+ IEnumerable<IExternalId> externalIds,
+ IEnumerable<IExternalUrlProvider> externalUrlProviders);
/// <summary>
/// Gets the available remote images.
@@ -141,6 +143,14 @@ namespace MediaBrowser.Controller.Providers
where T : BaseItem;
/// <summary>
+ /// Gets the metadata savers for the provided item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <returns>The metadata savers.</returns>
+ IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions);
+
+ /// <summary>
/// Gets all metadata plugins.
/// </summary>
/// <returns>IEnumerable{MetadataPlugin}.</returns>
diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs
index 3a97127ea..be3b25aee 100644
--- a/MediaBrowser.Controller/Providers/ItemInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemInfo.cs
@@ -11,6 +11,8 @@ namespace MediaBrowser.Controller.Providers
public ItemInfo(BaseItem item)
{
Path = item.Path;
+ ParentId = item.ParentId;
+ IndexNumber = item.IndexNumber;
ContainingFolderPath = item.ContainingFolderPath;
IsInMixedFolder = item.IsInMixedFolder;
@@ -27,6 +29,10 @@ namespace MediaBrowser.Controller.Providers
public string Path { get; set; }
+ public Guid ParentId { get; set; }
+
+ public int? IndexNumber { get; set; }
+
public string ContainingFolderPath { get; set; }
public VideoType VideoType { get; set; }
diff --git a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
index a07b3e898..733d40ba1 100644
--- a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
+++ b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
@@ -14,6 +14,6 @@ namespace MediaBrowser.Controller.Resolvers
/// <param name="fileInfo">The file information.</param>
/// <param name="parent">The parent BaseItem.</param>
/// <returns>True if the file should be ignored.</returns>
- bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent);
+ bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent);
}
}
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 3a12a56f1..9e3358818 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -134,6 +134,7 @@ namespace MediaBrowser.Controller.Session
/// <value>The now playing item.</value>
public BaseItemDto NowPlayingItem { get; set; }
+ [JsonIgnore]
public BaseItem FullNowPlayingItem { get; set; }
public BaseItemDto NowViewingItem { get; set; }
@@ -269,9 +270,7 @@ namespace MediaBrowser.Controller.Session
public void AddController(ISessionController controller)
{
- var controllers = SessionControllers.ToList();
- controllers.Add(controller);
- SessionControllers = controllers.ToArray();
+ SessionControllers = [..SessionControllers, controller];
}
public bool ContainsUser(Guid userId)