aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs8
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs19
-rw-r--r--MediaBrowser.Controller/Entities/Extensions.cs2
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs15
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs6
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs6
-rw-r--r--MediaBrowser.Controller/Entities/TagExtensions.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs11
-rw-r--r--MediaBrowser.Controller/IO/IPathManager.cs16
-rw-r--r--MediaBrowser.Controller/Library/IBatchLocalSimilarItemsProvider.cs26
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs15
-rw-r--r--MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs63
-rw-r--r--MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs62
-rw-r--r--MediaBrowser.Controller/Library/ISimilarItemsManager.cs70
-rw-r--r--MediaBrowser.Controller/Library/ISimilarItemsProvider.cs26
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs2
-rw-r--r--MediaBrowser.Controller/Library/SimilarItemReference.cs22
-rw-r--r--MediaBrowser.Controller/Library/SimilarItemsQuery.cs37
-rw-r--r--MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs32
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs6
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs208
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs56
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs2
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs15
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs9
-rw-r--r--MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs4
-rw-r--r--MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs7
-rw-r--r--MediaBrowser.Controller/Persistence/IPeopleRepository.cs8
-rw-r--r--MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs17
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs11
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs9
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs3
32 files changed, 661 insertions, 134 deletions
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
index 206b5ac426..8d5d54ffd9 100644
--- a/MediaBrowser.Controller/Collections/ICollectionManager.cs
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -58,6 +58,14 @@ namespace MediaBrowser.Controller.Collections
IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user);
/// <summary>
+ /// Gets the collections accessible to the supplied user that contain the provided item.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="itemId">The item identifier.</param>
+ /// <returns>The collections containing the item.</returns>
+ IEnumerable<BoxSet> GetCollectionsContainingItem(User user, Guid itemId);
+
+ /// <summary>
/// Gets the folder where collections are stored.
/// </summary>
/// <param name="createIfNeeded">Will create the collection folder on the storage if set to true.</param>
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 4cdcaabbb1..e24b60f69f 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -94,6 +94,8 @@ namespace MediaBrowser.Controller.Entities
private string _name;
+ private string _originalLanguage;
+
public const char SlugChar = '-';
protected BaseItem()
@@ -217,7 +219,11 @@ namespace MediaBrowser.Controller.Entities
public string OriginalTitle { get; set; }
[JsonIgnore]
- public string OriginalLanguage { get; set; }
+ public string OriginalLanguage
+ {
+ get => _originalLanguage;
+ set => _originalLanguage = LocalizationManager?.FindLanguageInfo(value)?.TwoLetterISOLanguageName ?? value;
+ }
/// <summary>
/// Gets or sets the id.
@@ -1564,7 +1570,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Gets the preferred metadata language.
+ /// Gets the preferred metadata country code.
/// </summary>
/// <returns>System.String.</returns>
public string GetPreferredMetadataCountryCode()
@@ -1598,6 +1604,15 @@ namespace MediaBrowser.Controller.Entities
return lang;
}
+ /// <summary>
+ /// Gets the original language of the item, inheriting from parent items if necessary.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ public virtual string GetInheritedOriginalLanguage()
+ {
+ return OriginalLanguage;
+ }
+
public virtual bool IsSaveLocalMetadataEnabled()
{
if (SourceType == SourceType.Channel)
diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs
index c56603a3eb..380041af84 100644
--- a/MediaBrowser.Controller/Entities/Extensions.cs
+++ b/MediaBrowser.Controller/Entities/Extensions.cs
@@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Entities
}
else
{
- item.RemoteTrailers = [..item.RemoteTrailers, mediaUrl];
+ item.RemoteTrailers = [.. item.RemoteTrailers, mediaUrl];
}
}
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index fa82ea8663..422c40ce5d 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities
AlbumArtistIds = [];
AlbumIds = [];
AncestorIds = [];
+ LinkedChildAncestorIds = [];
ArtistIds = [];
BlockUnratedItems = [];
BoxSetLibraryFolders = [];
@@ -58,6 +59,8 @@ namespace MediaBrowser.Controller.Entities
VideoTypes = [];
Years = [];
SkipDeserialization = false;
+ AudioLanguages = [];
+ SubtitleLanguages = [];
}
public InternalItemsQuery(User? user)
@@ -263,6 +266,12 @@ namespace MediaBrowser.Controller.Entities
public Guid[] AncestorIds { get; set; }
+ /// <summary>
+ /// Gets or sets a list of ancestor ids that the item's linked children must descend from.
+ /// Useful for filtering BoxSets/Playlists to only those that contain items from a specific library.
+ /// </summary>
+ public Guid[] LinkedChildAncestorIds { get; set; }
+
public Guid[] TopParentIds { get; set; }
public CollectionType?[] PresetViews { get; set; }
@@ -351,6 +360,8 @@ namespace MediaBrowser.Controller.Entities
public Dictionary<string, string>? HasAnyProviderId { get; set; }
+ public Dictionary<string, string[]>? HasAnyProviderIds { get; set; }
+
public Guid[] AlbumArtistIds { get; set; }
public Guid[] BoxSetLibraryFolders { get; set; }
@@ -385,6 +396,10 @@ namespace MediaBrowser.Controller.Entities
public bool IncludeExtras { get; set; }
+ public IReadOnlyList<string> AudioLanguages { get; set; }
+
+ public IReadOnlyList<string> SubtitleLanguages { get; set; }
+
public void SetUser(User user)
{
var maxRating = user.MaxParentalRatingScore;
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index dbe6f94dfd..42e4f79942 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -153,6 +153,12 @@ namespace MediaBrowser.Controller.Entities.TV
return 16.0 / 9;
}
+ /// <inheritdoc />
+ public override string GetInheritedOriginalLanguage()
+ {
+ return OriginalLanguage ?? Series?.GetInheritedOriginalLanguage();
+ }
+
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index f70f7dfb4c..e96ed05a5e 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -128,6 +128,12 @@ namespace MediaBrowser.Controller.Entities.TV
return result;
}
+ /// <inheritdoc />
+ public override string GetInheritedOriginalLanguage()
+ {
+ return OriginalLanguage ?? Series?.GetInheritedOriginalLanguage();
+ }
+
public override string CreatePresentationUniqueKey()
{
if (IndexNumber.HasValue)
diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs
index c1e4d1db2f..4ddba9835b 100644
--- a/MediaBrowser.Controller/Entities/TagExtensions.cs
+++ b/MediaBrowser.Controller/Entities/TagExtensions.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Entities
}
else
{
- item.Tags = [..current, name];
+ item.Tags = [.. current, name];
}
}
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 80bcd62dcd..44cae5197a 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -278,6 +278,17 @@ namespace MediaBrowser.Controller.Entities
return linkedVersionCount + localVersionCount + 1;
}
+ /// <inheritdoc />
+ public override string GetInheritedOriginalLanguage()
+ {
+ if (ExtraType.GetValueOrDefault() == Model.Entities.ExtraType.Trailer)
+ {
+ return GetOwner()?.GetInheritedOriginalLanguage();
+ }
+
+ return OriginalLanguage ?? GetOwner()?.GetInheritedOriginalLanguage();
+ }
+
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs
index eb67437545..30961c7610 100644
--- a/MediaBrowser.Controller/IO/IPathManager.cs
+++ b/MediaBrowser.Controller/IO/IPathManager.cs
@@ -22,30 +22,30 @@ public interface IPathManager
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="streamIndex">The stream index.</param>
/// <param name="extension">The subtitle file extension.</param>
- /// <returns>The absolute path.</returns>
- public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
+ /// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
+ public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
/// <summary>
/// Gets the path to the subtitle file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
- /// <returns>The absolute path.</returns>
- public string GetSubtitleFolderPath(string mediaSourceId);
+ /// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
+ public string? GetSubtitleFolderPath(string mediaSourceId);
/// <summary>
/// Gets the path to the attachment file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="fileName">The attachmentFileName index.</param>
- /// <returns>The absolute path.</returns>
- public string GetAttachmentPath(string mediaSourceId, string fileName);
+ /// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
+ public string? GetAttachmentPath(string mediaSourceId, string fileName);
/// <summary>
/// Gets the path to the attachment folder.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
- /// <returns>The absolute path.</returns>
- public string GetAttachmentFolderPath(string mediaSourceId);
+ /// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
+ public string? GetAttachmentFolderPath(string mediaSourceId);
/// <summary>
/// Gets the chapter images data path.
diff --git a/MediaBrowser.Controller/Library/IBatchLocalSimilarItemsProvider.cs b/MediaBrowser.Controller/Library/IBatchLocalSimilarItemsProvider.cs
new file mode 100644
index 0000000000..af49711606
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IBatchLocalSimilarItemsProvider.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// A local similar items provider that supports batch queries across multiple source items.
+/// Implementations share access filtering and entity loading across all sources for better performance.
+/// </summary>
+public interface IBatchLocalSimilarItemsProvider : ISimilarItemsProvider
+{
+ /// <summary>
+ /// Gets similar items for multiple source items in a single batch.
+ /// </summary>
+ /// <param name="sourceItems">The source items to find similar items for.</param>
+ /// <param name="query">The query options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Per-source-item results keyed by source item ID.</returns>
+ Task<Dictionary<Guid, IReadOnlyList<BaseItem>>> GetBatchSimilarItemsAsync(
+ IReadOnlyList<BaseItem> sourceItems,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index f5e3d7034e..0b64da291c 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -598,6 +598,14 @@ namespace MediaBrowser.Controller.Library
IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query);
/// <summary>
+ /// Gets the distinct people names per item for multiple items.
+ /// </summary>
+ /// <param name="itemIds">The item IDs.</param>
+ /// <param name="personTypes">The person types to include.</param>
+ /// <returns>A dictionary mapping each item ID to its distinct people names. Items with no matching people are omitted.</returns>
+ IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes);
+
+ /// <summary>
/// Queries the items.
/// </summary>
/// <param name="query">The query.</param>
@@ -784,5 +792,12 @@ namespace MediaBrowser.Controller.Library
/// <param name="query">The query filter.</param>
/// <returns>Aggregated filter values.</returns>
QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query);
+
+ /// <summary>
+ /// Gets a list of all language codes of the provided stream type.
+ /// </summary>
+ /// <param name="mediaStreamType">The stream type.</param>
+ /// <returns>List of language codes.</returns>
+ IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType);
}
}
diff --git a/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs b/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs
new file mode 100644
index 0000000000..b8e41ec810
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Provides similar items from the local library.
+/// Returns fully resolved BaseItems directly - no additional resolution needed.
+/// </summary>
+public interface ILocalSimilarItemsProvider : ISimilarItemsProvider
+{
+ /// <summary>
+ /// Determines whether the provider can handle items of the specified type.
+ /// </summary>
+ /// <param name="itemType">The item type.</param>
+ /// <returns><c>true</c> if the provider handles this item type; otherwise <c>false</c>.</returns>
+ bool Supports(Type itemType);
+
+ /// <summary>
+ /// Gets similar items from the local library.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions, etc.).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>The list of similar items from the library.</returns>
+ Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+}
+
+/// <summary>
+/// Provides similar items from the local library for a specific item type.
+/// Returns fully resolved BaseItems directly - no additional resolution needed.
+/// </summary>
+/// <typeparam name="TItemType">The type of item this provider handles.</typeparam>
+public interface ILocalSimilarItemsProvider<TItemType> : ILocalSimilarItemsProvider
+ where TItemType : BaseItem
+{
+ /// <summary>
+ /// Gets similar items from the local library.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions, etc.).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>The list of similar items from the library.</returns>
+ Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
+ TItemType item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+
+ bool ILocalSimilarItemsProvider.Supports(Type itemType)
+ => typeof(TItemType).IsAssignableFrom(itemType);
+
+ Task<IReadOnlyList<BaseItem>> ILocalSimilarItemsProvider.GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken)
+ => GetSimilarItemsAsync((TItemType)item, query, cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs b/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs
new file mode 100644
index 0000000000..3803e51769
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Provides similar item references from remote/external sources.
+/// Returns lightweight references with ProviderIds that the manager resolves to library items.
+/// </summary>
+public interface IRemoteSimilarItemsProvider : ISimilarItemsProvider
+{
+ /// <summary>
+ /// Determines whether the provider can handle items of the specified type.
+ /// </summary>
+ /// <param name="itemType">The item type.</param>
+ /// <returns><c>true</c> if the provider handles this item type; otherwise <c>false</c>.</returns>
+ bool Supports(Type itemType);
+
+ /// <summary>
+ /// Gets similar item references from an external source as an async stream.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>An async enumerable of similar item references.</returns>
+ IAsyncEnumerable<SimilarItemReference> GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+}
+
+/// <summary>
+/// Provides similar item references from remote/external sources for a specific item type.
+/// Returns lightweight references with ProviderIds that the manager resolves to library items.
+/// </summary>
+/// <typeparam name="TItemType">The type of item this provider handles.</typeparam>
+public interface IRemoteSimilarItemsProvider<TItemType> : IRemoteSimilarItemsProvider
+ where TItemType : BaseItem
+{
+ /// <summary>
+ /// Gets similar item references from an external source as an async stream.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>An async enumerable of similar item references.</returns>
+ IAsyncEnumerable<SimilarItemReference> GetSimilarItemsAsync(
+ TItemType item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+
+ bool IRemoteSimilarItemsProvider.Supports(Type itemType)
+ => typeof(TItemType).IsAssignableFrom(itemType);
+
+ IAsyncEnumerable<SimilarItemReference> IRemoteSimilarItemsProvider.GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken)
+ => GetSimilarItemsAsync((TItemType)item, query, cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/ISimilarItemsManager.cs b/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
new file mode 100644
index 0000000000..36fa547eeb
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Interface for managing similar items providers and operations.
+/// </summary>
+public interface ISimilarItemsManager
+{
+ /// <summary>
+ /// Registers similar items providers discovered through dependency injection.
+ /// </summary>
+ /// <param name="providers">The similar items providers to register.</param>
+ void AddParts(IEnumerable<ISimilarItemsProvider> providers);
+
+ /// <summary>
+ /// Gets the similar items providers for a specific item type.
+ /// </summary>
+ /// <typeparam name="T">The item type.</typeparam>
+ /// <returns>The list of similar items providers for that type.</returns>
+ IReadOnlyList<ISimilarItemsProvider> GetSimilarItemsProviders<T>()
+ where T : BaseItem;
+
+ /// <summary>
+ /// Gets similar items for the specified item.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="excludeArtistIds">Artist IDs to exclude from results.</param>
+ /// <param name="user">The user context.</param>
+ /// <param name="dtoOptions">The DTO options.</param>
+ /// <param name="limit">Maximum number of results.</param>
+ /// <param name="libraryOptions">The library options for provider configuration.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The list of similar items.</returns>
+ Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
+ BaseItem item,
+ IReadOnlyList<Guid> excludeArtistIds,
+ User? user,
+ DtoOptions dtoOptions,
+ int? limit,
+ LibraryOptions? libraryOptions,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Builds movie recommendations for a user: a mix of similar-items and person-based categories,
+ /// scheduled round-robin and capped to <paramref name="categoryLimit"/>.
+ /// </summary>
+ /// <param name="user">The user the recommendations are for. May be <see langword="null"/> for anonymous access.</param>
+ /// <param name="parentId">The library/folder to localize the search to. Pass <see cref="Guid.Empty"/> to use the root.</param>
+ /// <param name="categoryLimit">Maximum number of recommendation categories to return.</param>
+ /// <param name="itemLimit">Maximum number of items per category.</param>
+ /// <param name="dtoOptions">DTO options used when querying the library.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The list of recommendation categories, ordered by <see cref="RecommendationType"/>.</returns>
+ Task<IReadOnlyList<SimilarItemsRecommendation>> GetMovieRecommendationsAsync(
+ User? user,
+ Guid parentId,
+ int categoryLimit,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/ISimilarItemsProvider.cs b/MediaBrowser.Controller/Library/ISimilarItemsProvider.cs
new file mode 100644
index 0000000000..0d089369a8
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ISimilarItemsProvider.cs
@@ -0,0 +1,26 @@
+using System;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Base marker interface for similar items providers.
+/// </summary>
+public interface ISimilarItemsProvider
+{
+ /// <summary>
+ /// Gets the name of the provider.
+ /// </summary>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the type of the provider.
+ /// </summary>
+ MetadataPluginType Type { get; }
+
+ /// <summary>
+ /// Gets the cache duration for results from this provider.
+ /// If null, results will not be cached.
+ /// </summary>
+ TimeSpan? CacheDuration => null;
+}
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index b558ef73d5..c5e7ae4913 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.Library
get
{
var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : [Path];
- return AdditionalLocations is null ? paths : [..paths, ..AdditionalLocations];
+ return AdditionalLocations is null ? paths : [.. paths, .. AdditionalLocations];
}
}
diff --git a/MediaBrowser.Controller/Library/SimilarItemReference.cs b/MediaBrowser.Controller/Library/SimilarItemReference.cs
new file mode 100644
index 0000000000..2a40c93bdd
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SimilarItemReference.cs
@@ -0,0 +1,22 @@
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// A reference to a similar item by provider ID with a similarity score.
+/// </summary>
+public class SimilarItemReference
+{
+ /// <summary>
+ /// Gets or sets the provider name (e.g., "Tmdb", "MusicBrainzArtist").
+ /// </summary>
+ public required string ProviderName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the provider ID value.
+ /// </summary>
+ public required string ProviderId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the similarity score (0.0 to 1.0).
+ /// </summary>
+ public float? Score { get; set; }
+}
diff --git a/MediaBrowser.Controller/Library/SimilarItemsQuery.cs b/MediaBrowser.Controller/Library/SimilarItemsQuery.cs
new file mode 100644
index 0000000000..1ed3ceec16
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SimilarItemsQuery.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Controller.Dto;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Query options for similar items requests.
+/// </summary>
+public class SimilarItemsQuery
+{
+ /// <summary>
+ /// Gets or sets the user context.
+ /// </summary>
+ public User? User { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum number of results.
+ /// </summary>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets the DTO options.
+ /// </summary>
+ public DtoOptions? DtoOptions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item IDs to exclude from results.
+ /// </summary>
+ public IReadOnlyList<Guid> ExcludeItemIds { get; set; } = [];
+
+ /// <summary>
+ /// Gets or sets the artist IDs to exclude from results.
+ /// </summary>
+ public IReadOnlyList<Guid> ExcludeArtistIds { get; set; } = [];
+}
diff --git a/MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs b/MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs
new file mode 100644
index 0000000000..71346fcadf
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// A recommendation category derived from a baseline item, holding similar items prior to DTO conversion.
+/// </summary>
+public sealed class SimilarItemsRecommendation
+{
+ /// <summary>
+ /// Gets the display name of the baseline item the recommendation is based on.
+ /// </summary>
+ public required string BaselineItemName { get; init; }
+
+ /// <summary>
+ /// Gets an identifier for the recommendation category.
+ /// </summary>
+ public required Guid CategoryId { get; init; }
+
+ /// <summary>
+ /// Gets the recommendation type.
+ /// </summary>
+ public required RecommendationType RecommendationType { get; init; }
+
+ /// <summary>
+ /// Gets the similar items for the baseline, ordered by relevance.
+ /// </summary>
+ public required IReadOnlyList<BaseItem> Items { get; init; }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index 10f2f04af6..34826982af 100644
--- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -92,6 +92,12 @@ namespace MediaBrowser.Controller.MediaEncoding
public string CodecTag { get; set; }
/// <summary>
+ /// Gets or sets the rotation.
+ /// </summary>
+ /// <value>The video rotation angle, usually 0 or +-90/180.</value>
+ public string Rotation { get; set; }
+
+ /// <summary>
/// Gets or sets the framerate.
/// </summary>
/// <value>The framerate.</value>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index a0e04eae63..ff8d84d45e 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -86,6 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0);
+ private readonly Version _minFFmpegNoiseBsfDrop = new Version(5, 0);
private static readonly string[] _videoProfilesH264 =
[
@@ -1267,16 +1268,13 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append(_mediaEncoder.GetInputPathArgument(state));
}
- // sub2video for external graphical subtitles
- if (state.SubtitleStream is not null
- && ShouldEncodeSubtitle(state)
- && !state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleStream.IsExternal)
+ if (NeedsExternalSubtitleMuxing(state))
{
var subtitlePath = state.SubtitleStream.Path;
- var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
+ var isGraphicalBurnIn = ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream;
// dvdsub/vobsub graphical subtitles use .sub+.idx pairs
+ var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
{
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
@@ -1307,7 +1305,7 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(' ').Append(seekSubParam);
}
- if (!string.IsNullOrEmpty(canvasArgs))
+ if (isGraphicalBurnIn && !string.IsNullOrEmpty(canvasArgs))
{
arg.Append(canvasArgs);
}
@@ -1550,20 +1548,61 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
{
- var bitStreamArgs = string.Empty;
+ var filters = new List<string>();
+
+ var noiseFilter = GetCopiedAudioTrimBsf(state);
+ if (!string.IsNullOrEmpty(noiseFilter))
+ {
+ filters.Add(noiseFilter);
+ }
+
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(mediaSourceContainer, "ts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
+ || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase))
+ && IsAAC(state.AudioStream))
{
- bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Audio);
- bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
+ filters.Add("aac_adtstoasc");
}
- return bitStreamArgs;
+ return filters.Count == 0
+ ? string.Empty
+ : " -bsf:a " + string.Join(',', filters);
+ }
+
+ // When video is transcoded, accurate_seek (the default) trims video to the
+ // exact seek point via decoder-side frame discard. But stream-copied audio
+ // bypasses the decoder, so it starts from the nearest keyframe — potentially
+ // seconds before the target. Use the noise bsf to drop copied audio packets
+ // before the seek target, achieving the same trim precision without
+ // re-encoding. The noise bsf's drop= parameter requires ffmpeg >= 5.0.
+ // Important: make sure not to use it with wtv because it breaks seeking
+ private string GetCopiedAudioTrimBsf(EncodingJobInfo state)
+ {
+ if (state.TranscodingType is not TranscodingJobType.Hls
+ || !state.IsVideoRequest
+ || IsCopyCodec(state.OutputVideoCodec)
+ || !IsCopyCodec(state.OutputAudioCodec)
+ || string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase)
+ || _mediaEncoder.EncoderVersion < _minFFmpegNoiseBsfDrop)
+ {
+ return null;
+ }
+
+ var startTicks = state.BaseRequest.StartTimeTicks ?? 0;
+ if (startTicks <= 0)
+ {
+ return null;
+ }
+
+ var seekSeconds = startTicks / (double)TimeSpan.TicksPerSecond;
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "noise=drop='lt(pts*tb\\,{0:F3})'",
+ seekSeconds);
}
public static string GetSegmentFileExtension(string segmentContainer)
@@ -1645,10 +1684,9 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
- // Override the too high default qmin 18 in transcoding preset
+ // Override the too high default qmin 18 in transcoding preset in legacy h26x_amf
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
@@ -1767,13 +1805,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += encoderPreset switch
{
- EncoderPreset.veryslow => " -preset p7",
- EncoderPreset.slower => " -preset p6",
- EncoderPreset.slow => " -preset p5",
- EncoderPreset.medium => " -preset p4",
- EncoderPreset.fast => " -preset p3",
- EncoderPreset.faster => " -preset p2",
- _ => " -preset p1"
+ EncoderPreset.veryslow => " -preset p7",
+ EncoderPreset.slower => " -preset p6",
+ EncoderPreset.slow => " -preset p5",
+ EncoderPreset.medium => " -preset p4",
+ EncoderPreset.fast => " -preset p3",
+ EncoderPreset.faster => " -preset p2",
+ _ => " -preset p1"
};
}
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
@@ -1783,11 +1821,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += encoderPreset switch
{
- EncoderPreset.veryslow => " -quality quality",
- EncoderPreset.slower => " -quality quality",
- EncoderPreset.slow => " -quality quality",
- EncoderPreset.medium => " -quality balanced",
- _ => " -quality speed"
+ EncoderPreset.veryslow => " -quality quality",
+ EncoderPreset.slower => " -quality quality",
+ EncoderPreset.slow => " -quality quality",
+ EncoderPreset.medium => " -quality balanced",
+ _ => " -quality speed"
};
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
@@ -1807,11 +1845,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += encoderPreset switch
{
- EncoderPreset.veryslow => " -prio_speed 0",
- EncoderPreset.slower => " -prio_speed 0",
- EncoderPreset.slow => " -prio_speed 0",
- EncoderPreset.medium => " -prio_speed 0",
- _ => " -prio_speed 1"
+ EncoderPreset.veryslow => " -prio_speed 0",
+ EncoderPreset.slower => " -prio_speed 0",
+ EncoderPreset.slow => " -prio_speed 0",
+ EncoderPreset.medium => " -prio_speed 0",
+ _ => " -prio_speed 1"
};
}
@@ -1880,10 +1918,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty;
var fontPath = _pathManager.GetAttachmentFolderPath(state.MediaSource.Id);
- var fontParam = string.Format(
- CultureInfo.InvariantCulture,
- ":fontsdir='{0}'",
- _mediaEncoder.EscapeSubtitleFilterPath(fontPath));
+ var fontParam = fontPath is null
+ ? string.Empty
+ : string.Format(
+ CultureInfo.InvariantCulture,
+ ":fontsdir='{0}'",
+ _mediaEncoder.EscapeSubtitleFilterPath(fontPath));
if (state.SubtitleStream.IsExternal)
{
@@ -2016,11 +2056,15 @@ namespace MediaBrowser.Controller.MediaEncoding
args += keyFrameArg + gopArg;
}
- // global_header produced by AMD HEVC VA-API encoder causes non-playable fMP4 on iOS
+ // The in-band Parameter Sets generated by the AMD HEVC VA-API encoder is inconsistent
+ // with the extradata generated by ffmpeg, causing decoding failures when using hvc1.
if (string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
&& _mediaEncoder.IsVaapiDeviceAmd)
{
- args += " -flags:v -global_header";
+ // Extracting the extradata from the in-band PS to bypass the issue.
+ // This can be removed once the issue is resolved in libva or Mesa.
+ // Transcoding is unavoidable here, so using BSF will not conflict with BSF in remuxing.
+ args += " -flags:v -global_header -bsf:v extract_extradata=remove=0";
}
return args;
@@ -2466,6 +2510,17 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ var requestedRotations = state.GetRequestedRotations(videoStream.Codec);
+ if (requestedRotations.Length > 0)
+ {
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ if (rotation != 0
+ && !requestedRotations.Contains(rotation.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal))
+ {
+ return false;
+ }
+ }
+
// Video width must fall within requested value
if (request.MaxWidth.HasValue
&& (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value))
@@ -2750,25 +2805,29 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{
+#pragma warning disable SA1008
return (inputChannels, outputChannels) switch
{
- (>= 6, >= 6 or 0) => Math.Min(640000, bitrate),
- (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
- (> 0, _) => Math.Min(inputChannels * 128000, bitrate),
+ ( >= 6, >= 6 or 0) => Math.Min(640000, bitrate),
+ ( > 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
+ ( > 0, _) => Math.Min(inputChannels * 128000, bitrate),
(_, _) => Math.Min(384000, bitrate)
};
+#pragma warning restore SA1008
}
if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase))
{
+#pragma warning disable SA1008
return (inputChannels, outputChannels) switch
{
- (>= 6, >= 6 or 0) => Math.Min(768000, bitrate),
- (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
- (> 0, _) => Math.Min(inputChannels * 136000, bitrate),
+ ( >= 6, >= 6 or 0) => Math.Min(768000, bitrate),
+ ( > 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
+ ( > 0, _) => Math.Min(inputChannels * 136000, bitrate),
(_, _) => Math.Min(672000, bitrate)
};
+#pragma warning restore SA1008
}
// Empty bitrate area is not allow on iOS
@@ -2989,23 +3048,6 @@ namespace MediaBrowser.Controller.MediaEncoding
}
seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick));
-
- if (state.IsVideoRequest)
- {
- // If we are remuxing, then the copied stream cannot be seeked accurately (it will seek to the nearest
- // keyframe). If we are using fMP4, then force all other streams to use the same inaccurate seeking to
- // avoid A/V sync issues which cause playback issues on some devices.
- // When remuxing video, the segment start times correspond to key frames in the source stream, so this
- // option shouldn't change the seeked point that much.
- // Important: make sure not to use it with wtv because it breaks seeking
- if (state.TranscodingType is TranscodingJobType.Hls
- && string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)
- && (IsCopyCodec(state.OutputVideoCodec) || IsCopyCodec(state.OutputAudioCodec))
- && !string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase))
- {
- seekParam += " -noaccurate_seek";
- }
- }
}
return seekParam;
@@ -3060,11 +3102,8 @@ namespace MediaBrowser.Controller.MediaEncoding
int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
if (state.AudioStream.IsExternal)
{
- bool hasExternalGraphicsSubs = state.SubtitleStream is not null
- && ShouldEncodeSubtitle(state)
- && state.SubtitleStream.IsExternal
- && !state.SubtitleStream.IsTextSubtitleStream;
- int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
+ bool hasExternalSubAsInput = NeedsExternalSubtitleMuxing(state);
+ int externalAudioMapIndex = hasExternalSubAsInput ? 2 : 1;
args += string.Format(
CultureInfo.InvariantCulture,
@@ -3092,12 +3131,31 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (subtitleMethod == SubtitleDeliveryMethod.Embed)
{
- int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+ if (state.SubtitleStream.IsExternal)
+ {
+ // External subtitle file is added as second FFmpeg input.
+ // For single-stream files (SRT/ASS/VTT) the in-file index is always 0.
+ // For multi-stream containers (MKS) we count how many streams from
+ // the same file appear before the selected one.
+ var inFileIndex = state.MediaSource.MediaStreams
+ .Where(s => string.Equals(s.Path, state.SubtitleStream.Path, StringComparison.Ordinal))
+ .TakeWhile(s => s.Index != state.SubtitleStream.Index)
+ .Count();
- args += string.Format(
- CultureInfo.InvariantCulture,
- " -map 0:{0}",
- subtitleStreamIndex);
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 1:{0}",
+ inFileIndex);
+ }
+ else
+ {
+ int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 0:{0}",
+ subtitleStreamIndex);
+ }
}
else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
{
@@ -7874,6 +7932,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|| (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec));
}
+ private static bool NeedsExternalSubtitleMuxing(EncodingJobInfo state)
+ {
+ return state.SubtitleStream is not null
+ && state.SubtitleStream.IsExternal
+ && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed
+ || (ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream));
+ }
+
public static string GetVideoSyncOption(string videoSync, Version encoderVersion)
{
if (string.IsNullOrEmpty(videoSync))
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 7d0384ef27..3a1897a244 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -571,62 +571,50 @@ namespace MediaBrowser.Controller.MediaEncoding
public string[] GetRequestedProfiles(string codec)
{
- if (!string.IsNullOrEmpty(BaseRequest.Profile))
- {
- return BaseRequest.Profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ var profile = BaseRequest.Profile;
- if (!string.IsNullOrEmpty(codec))
+ if (string.IsNullOrEmpty(profile) && !string.IsNullOrEmpty(codec))
{
- var profile = BaseRequest.GetOption(codec, "profile");
-
- if (!string.IsNullOrEmpty(profile))
- {
- return profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ profile = BaseRequest.GetOption(codec, "profile");
}
- return Array.Empty<string>();
+ return (profile ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetRequestedRangeTypes(string codec)
{
- if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
- {
- return BaseRequest.VideoRangeType.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ var rangetype = BaseRequest.VideoRangeType;
- if (!string.IsNullOrEmpty(codec))
+ if (string.IsNullOrEmpty(rangetype) && !string.IsNullOrEmpty(codec))
{
- var rangetype = BaseRequest.GetOption(codec, "rangetype");
-
- if (!string.IsNullOrEmpty(rangetype))
- {
- return rangetype.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ rangetype = BaseRequest.GetOption(codec, "rangetype");
}
- return Array.Empty<string>();
+ return (rangetype ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetRequestedCodecTags(string codec)
{
- if (!string.IsNullOrEmpty(BaseRequest.CodecTag))
+ var codectag = BaseRequest.CodecTag;
+
+ if (string.IsNullOrEmpty(codectag) && !string.IsNullOrEmpty(codec))
{
- return BaseRequest.CodecTag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
+ codectag = BaseRequest.GetOption(codec, "codectag");
}
- if (!string.IsNullOrEmpty(codec))
- {
- var codectag = BaseRequest.GetOption(codec, "codectag");
+ return (codectag ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
+ }
- if (!string.IsNullOrEmpty(codectag))
- {
- return codectag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ public string[] GetRequestedRotations(string codec)
+ {
+ var rotation = BaseRequest.Rotation;
+
+ if (string.IsNullOrEmpty(rotation) && !string.IsNullOrEmpty(codec))
+ {
+ rotation = BaseRequest.GetOption(codec, "rotation");
}
- return Array.Empty<string>();
+ return (rotation ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string GetRequestedLevel(string codec)
diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
index 54da218530..9bee653e2e 100644
--- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
+++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 1e0d77fe51..2bcce168cf 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -40,11 +40,6 @@ namespace MediaBrowser.Controller.Net
/// </summary>
private readonly List<(IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)> _activeConnections = new();
- /// <summary>
- /// The logger.
- /// </summary>
- protected readonly ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
-
private readonly Task _messageConsumerTask;
protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
@@ -57,6 +52,11 @@ namespace MediaBrowser.Controller.Net
}
/// <summary>
+ /// Gets the Logger.
+ /// </summary>
+ protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger { get; }
+
+ /// <summary>
/// Gets the type used for the messages sent to the client.
/// </summary>
/// <value>The type.</value>
@@ -209,6 +209,11 @@ namespace MediaBrowser.Controller.Net
var (connection, cts, state) = tuple;
var cancellationToken = cts.Token;
+ // Restore the culture context captured when the connection was established
+ // so that GetDataToSendForConnection produces a localized payload matching
+ // the client's Accept-Language preference rather than the server default.
+ connection.ApplyRequestCulture();
+
var data = await GetDataToSendForConnection(connection).ConfigureAwait(false);
if (data is null)
{
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index bdc0f9a10f..48431e75c3 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -77,5 +77,14 @@ namespace MediaBrowser.Controller.Net
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task ReceiveAsync(CancellationToken cancellationToken = default);
+
+ /// <summary>
+ /// Applies the culture context captured when the connection was established
+ /// (from the upgrade request's <c>Accept-Language</c> header) to the current
+ /// async flow. Server-initiated message senders should call this before
+ /// localising any payload so that the response uses the client's preferred
+ /// language rather than the server default.
+ /// </summary>
+ void ApplyRequestCulture();
}
}
diff --git a/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs b/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs
index d0cddf54a6..a4614fc125 100644
--- a/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs
+++ b/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using LinkedChildType = MediaBrowser.Controller.Entities.LinkedChildType;
@@ -29,8 +30,9 @@ public interface ILinkedChildrenService
/// Gets parent IDs that reference the specified child with LinkedChildType.Manual.
/// </summary>
/// <param name="childId">The child item ID.</param>
+ /// <param name="parentType">Optional parent item type filter.</param>
/// <returns>List of parent IDs that reference the child.</returns>
- IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId);
+ IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId, BaseItemKind? parentType = null);
/// <summary>
/// Updates LinkedChildren references from one child to another.
diff --git a/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs
index 665129eafd..de04ff021d 100644
--- a/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs
@@ -22,6 +22,13 @@ public interface IMediaStreamRepository
IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter);
/// <summary>
+ /// Gets all language codes of the provided stream type.
+ /// </summary>
+ /// <param name="mediaStreamType">The type of the media stream.</param>
+ /// <returns>IEnumerable{string}.</returns>
+ IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType);
+
+ /// <summary>
/// Saves the media streams.
/// </summary>
/// <param name="id">The identifier.</param>
diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
index a89f3ef9ee..e2833dc722 100644
--- a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
@@ -32,4 +32,12 @@ public interface IPeopleRepository
/// <param name="filter">The query.</param>
/// <returns>The list of people names matching the filter.</returns>
IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter);
+
+ /// <summary>
+ /// Gets the distinct people names per item for multiple items efficiently by querying from the mapping table.
+ /// </summary>
+ /// <param name="itemIds">The item IDs to get people for.</param>
+ /// <param name="personTypes">The person types to include (e.g. "Actor", "Director").</param>
+ /// <returns>A dictionary mapping each item ID to its distinct people names, ordered by cast list order. Items with no matching people are omitted.</returns>
+ IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes);
}
diff --git a/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs b/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs
new file mode 100644
index 0000000000..4196cd9f24
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Controller.Plugins;
+
+/// <summary>
+/// Marker interface for integrated/bundled plugins that ship their plugin image as an embedded
+/// resource inside the plugin assembly rather than as a file on disk.
+/// </summary>
+/// <remarks>
+/// This interface is intended for plugins compiled into the server. External plugins should
+/// continue to declare their image via the <c>imagePath</c> field in <c>meta.json</c>.
+/// </remarks>
+public interface IHasEmbeddedImage
+{
+ /// <summary>
+ /// Gets the name of the embedded resource in this plugin's assembly to serve as the plugin image.
+ /// </summary>
+ string ImageResourceName { get; }
+}
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 0d3a334dfb..c87f09a117 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -144,6 +144,17 @@ namespace MediaBrowser.Controller.Providers
where T : BaseItem;
/// <summary>
+ /// Gets the metadata providers for the provided item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="includeDisabled">Whether to include disabled providers.</param>
+ /// <typeparam name="T">The type of metadata provider.</typeparam>
+ /// <returns>The metadata providers.</returns>
+ IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions, bool includeDisabled)
+ where T : BaseItem;
+
+ /// <summary>
/// Gets the metadata savers for the provided item.
/// </summary>
/// <param name="item">The item.</param>
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 96783f6073..fb68bfb770 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -45,7 +45,6 @@ namespace MediaBrowser.Controller.Session
PlayState = new PlayerStateInfo();
SessionControllers = [];
NowPlayingQueue = [];
- NowPlayingQueueFullItems = [];
}
/// <summary>
@@ -272,15 +271,9 @@ namespace MediaBrowser.Controller.Session
public IReadOnlyList<QueueItem> NowPlayingQueue { get; set; }
/// <summary>
- /// Gets or sets the now playing queue full items.
- /// </summary>
- /// <value>The now playing queue full items.</value>
- public IReadOnlyList<BaseItemDto> NowPlayingQueueFullItems { get; set; }
-
- /// <summary>
/// Gets or sets a value indicating whether the session has a custom device name.
/// </summary>
- /// <value><c>true</c> if this session has a custom device name; otherwise, <c>false</c>.</value>
+ /// <value><c>true</c> if the session has a custom device name; otherwise, <c>false</c>.</value>
public bool HasCustomDeviceName { get; set; }
/// <summary>
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
index 132765b719..eb38eeb503 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
@@ -141,7 +141,8 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
_logger.LogError("Unable to set playing queue in group {GroupId}.", context.GroupId.ToString());
// Ignore request and return to previous state.
- IGroupState newState = prevState switch {
+ IGroupState newState = prevState switch
+ {
GroupStateType.Playing => new PlayingGroupState(LoggerFactory),
GroupStateType.Paused => new PausedGroupState(LoggerFactory),
_ => new IdleGroupState(LoggerFactory)