aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
authorDaniel Țuțuianu <tutuianu_daniel@yahoo.com>2026-06-17 06:16:42 +0300
committerDaniel Țuțuianu <tutuianu_daniel@yahoo.com>2026-06-17 06:16:42 +0300
commit1ea525a4083dbdc929605eb0eb5c6add93bc8392 (patch)
tree97056e3e9b8e06ae825199214ec3f9d34b53e4c8 /MediaBrowser.Controller
parent372c1681d8272c6fa8f120a132bc40351067fb10 (diff)
parent3307406ac8d7aa62184f99946f69a1cbf92a060b (diff)
Merge branch 'master' into fix/livetv-channel-icon-refresh
Resolve GuideManager conflict by keeping LiveTvChannelImageHelper so channel icons re-fetch on every guide refresh, including when the URL is unchanged.
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs8
-rw-r--r--MediaBrowser.Controller/Dto/DtoOptions.cs56
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs42
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs7
-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.cs1
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs18
-rw-r--r--MediaBrowser.Controller/IO/IExternalDataManager.cs7
-rw-r--r--MediaBrowser.Controller/Library/IBatchLocalSimilarItemsProvider.cs26
-rw-r--r--MediaBrowser.Controller/Library/IExternalSearchProvider.cs20
-rw-r--r--MediaBrowser.Controller/Library/IInternalSearchProvider.cs8
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs8
-rw-r--r--MediaBrowser.Controller/Library/ISearchEngine.cs18
-rw-r--r--MediaBrowser.Controller/Library/ISearchManager.cs48
-rw-r--r--MediaBrowser.Controller/Library/ISearchProvider.cs44
-rw-r--r--MediaBrowser.Controller/Library/ISimilarItemsManager.cs20
-rw-r--r--MediaBrowser.Controller/Library/SearchProviderQuery.cs45
-rw-r--r--MediaBrowser.Controller/Library/SearchResult.cs60
-rw-r--r--MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs32
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs143
-rw-r--r--MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs4
-rw-r--r--MediaBrowser.Controller/Persistence/IPeopleRepository.cs8
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs9
24 files changed, 531 insertions, 113 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/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs
index a71cdbd62c..d319feb6b2 100644
--- a/MediaBrowser.Controller/Dto/DtoOptions.cs
+++ b/MediaBrowser.Controller/Dto/DtoOptions.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,13 +6,16 @@ using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Dto
{
+ /// <summary>
+ /// Options that control which fields and images are populated when building a <see cref="MediaBrowser.Model.Dto.BaseItemDto"/>.
+ /// </summary>
public class DtoOptions
{
- private static readonly ItemFields[] DefaultExcludedFields = new[]
- {
+ private static readonly ItemFields[] DefaultExcludedFields =
+ [
ItemFields.SeasonUserData,
ItemFields.RefreshState
- };
+ ];
private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
@@ -22,11 +23,18 @@ namespace MediaBrowser.Controller.Dto
.Except(DefaultExcludedFields)
.ToArray();
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DtoOptions"/> class with all fields enabled.
+ /// </summary>
public DtoOptions()
: this(true)
{
}
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DtoOptions"/> class.
+ /// </summary>
+ /// <param name="allFields">Whether to populate all available fields.</param>
public DtoOptions(bool allFields)
{
ImageTypeLimit = int.MaxValue;
@@ -38,23 +46,61 @@ namespace MediaBrowser.Controller.Dto
ImageTypes = AllImageTypes;
}
+ /// <summary>
+ /// Gets or sets the fields to populate on the DTO.
+ /// </summary>
public IReadOnlyList<ItemFields> Fields { get; set; }
+ /// <summary>
+ /// Gets or sets the image types to populate on the DTO.
+ /// </summary>
public IReadOnlyList<ImageType> ImageTypes { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum number of images to return per image type.
+ /// </summary>
public int ImageTypeLimit { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether image information is populated.
+ /// </summary>
public bool EnableImages { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether program recording information is populated.
+ /// </summary>
public bool AddProgramRecordingInfo { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether user data is populated.
+ /// </summary>
public bool EnableUserData { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the currently airing program is populated.
+ /// </summary>
public bool AddCurrentProgram { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether an episode's portrait poster (its season's primary
+ /// image, falling back to the series') should replace the episode's own (16:9) primary image.
+ /// Used by views that render episodes as poster cards, e.g. "Latest".
+ /// </summary>
+ public bool PreferEpisodeParentPoster { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether the specified field is populated.
+ /// </summary>
+ /// <param name="field">The field to check.</param>
+ /// <returns><c>true</c> if the field is populated; otherwise, <c>false</c>.</returns>
public bool ContainsField(ItemFields field)
=> Fields.Contains(field);
+ /// <summary>
+ /// Gets the number of images to return for the specified image type.
+ /// </summary>
+ /// <param name="type">The image type.</param>
+ /// <returns>The image limit for the type, or 0 if the type is not enabled.</returns>
public int GetImageLimit(ImageType type)
{
if (EnableImages && ImageTypes.Contains(type))
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 4cdcaabbb1..21304768bd 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
@@ -94,6 +93,8 @@ namespace MediaBrowser.Controller.Entities
private string _name;
+ private string _originalLanguage;
+
public const char SlugChar = '-';
protected BaseItem()
@@ -217,7 +218,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.
@@ -1128,15 +1133,7 @@ namespace MediaBrowser.Controller.Entities
ArgumentNullException.ThrowIfNull(item);
var protocol = item.PathProtocol;
-
- // Resolve the item path so everywhere we use the media source it will always point to
- // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
- // path will return null, so it's safe to check for all paths.
var itemPath = item.Path;
- if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo)
- {
- itemPath = linkInfo.FullName;
- }
var info = new MediaSourceInfo
{
@@ -1564,7 +1561,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 +1595,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)
@@ -2712,7 +2718,7 @@ namespace MediaBrowser.Controller.Entities
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();
+ return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, orderBy).ToArray();
}
public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
@@ -2722,16 +2728,17 @@ namespace MediaBrowser.Controller.Entities
public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> orderBy)
{
- return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user, orderBy).ToArray();
+ return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user, orderBy).ToArray();
}
/// <summary>
/// Get all extras associated with this item, sorted by <see cref="SortName"/>.
/// </summary>
+ /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</param>
/// <returns>An enumerable containing the items.</returns>
- public IEnumerable<BaseItem> GetExtras()
+ public IEnumerable<BaseItem> GetExtras(User user = null)
{
- return LibraryManager.GetItemList(new InternalItemsQuery()
+ return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
OwnerIds = [Id],
OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
@@ -2742,10 +2749,11 @@ namespace MediaBrowser.Controller.Entities
/// Get all extras with specific types that are associated with this item.
/// </summary>
/// <param name="extraTypes">The types of extras to retrieve.</param>
+ /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</param>
/// <returns>An enumerable containing the extras.</returns>
- public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes)
+ public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes, User user = null)
{
- return LibraryManager.GetItemList(new InternalItemsQuery()
+ return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
OwnerIds = [Id],
ExtraTypes = extraTypes.ToArray(),
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 5fa1213db3..25cbcedc5f 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -906,7 +906,10 @@ namespace MediaBrowser.Controller.Entities
query.Parent = this;
}
- if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet)
+ // BoxSets and Playlists can have per-user visibility (shares/open access) that is stored in the
+ // serialized item data and cannot be evaluated by the database query, so filter them in memory.
+ if (query.IncludeItemTypes.Length > 0
+ && query.IncludeItemTypes.All(t => t == BaseItemKind.BoxSet || t == BaseItemKind.Playlist))
{
return QueryWithPostFiltering(query);
}
@@ -927,7 +930,7 @@ namespace MediaBrowser.Controller.Entities
if (user is not null)
{
- // needed for boxsets
+ // needed for boxsets and playlists
itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User));
}
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 4ddba9835b..07c2298fce 100644
--- a/MediaBrowser.Controller/Entities/TagExtensions.cs
+++ b/MediaBrowser.Controller/Entities/TagExtensions.cs
@@ -15,6 +15,7 @@ namespace MediaBrowser.Controller.Entities
throw new ArgumentNullException(nameof(name));
}
+ name = name.Trim();
var current = item.Tags;
if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 80bcd62dcd..e7a5672ebd 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -10,6 +10,7 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@@ -278,6 +279,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();
@@ -379,13 +391,13 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the additional parts.
/// </summary>
+ /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</param>
/// <returns>IEnumerable{Video}.</returns>
- public IOrderedEnumerable<Video> GetAdditionalParts()
+ public IOrderedEnumerable<Video> GetAdditionalParts(User user = null)
{
return GetAdditionalPartIds()
- .Select(i => LibraryManager.GetItemById(i))
+ .Select(i => LibraryManager.GetItemById<Video>(i, user))
.Where(i => i is not null)
- .OfType<Video>()
.OrderBy(i => i.SortName);
}
diff --git a/MediaBrowser.Controller/IO/IExternalDataManager.cs b/MediaBrowser.Controller/IO/IExternalDataManager.cs
index f69f4586c6..b2eb8fc3f1 100644
--- a/MediaBrowser.Controller/IO/IExternalDataManager.cs
+++ b/MediaBrowser.Controller/IO/IExternalDataManager.cs
@@ -16,4 +16,11 @@ public interface IExternalDataManager
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Deletes only the filesystem-side external item data (attachments, subtitles, trickplay, chapter images).
+ /// Use this when DB-side cleanup is already handled by another code path (e.g. <c>IItemPersistenceService.DeleteItem</c>).
+ /// </summary>
+ /// <param name="item">The item.</param>
+ void DeleteExternalItemFiles(BaseItem item);
}
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/IExternalSearchProvider.cs b/MediaBrowser.Controller/Library/IExternalSearchProvider.cs
new file mode 100644
index 0000000000..bded8ba3a3
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IExternalSearchProvider.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Interface for external search providers that offer enhanced search capabilities.
+/// </summary>
+public interface IExternalSearchProvider : ISearchProvider
+{
+ /// <summary>
+ /// Searches for items matching the query.
+ /// </summary>
+ /// <param name="query">The search query.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>Async enumerable of search results with relevance scores.</returns>
+ new IAsyncEnumerable<SearchResult> SearchAsync(
+ SearchProviderQuery query,
+ CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/IInternalSearchProvider.cs b/MediaBrowser.Controller/Library/IInternalSearchProvider.cs
new file mode 100644
index 0000000000..f87931395d
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IInternalSearchProvider.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Marker interface for internal search providers that typically query the local database directly.
+/// </summary>
+public interface IInternalSearchProvider : ISearchProvider
+{
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index f4c2196400..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>
diff --git a/MediaBrowser.Controller/Library/ISearchEngine.cs b/MediaBrowser.Controller/Library/ISearchEngine.cs
deleted file mode 100644
index 31dcbba5bd..0000000000
--- a/MediaBrowser.Controller/Library/ISearchEngine.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Search;
-
-namespace MediaBrowser.Controller.Library
-{
- /// <summary>
- /// Interface ILibrarySearchEngine.
- /// </summary>
- public interface ISearchEngine
- {
- /// <summary>
- /// Gets the search hints.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>Task{IEnumerable{SearchHintInfo}}.</returns>
- QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query);
- }
-}
diff --git a/MediaBrowser.Controller/Library/ISearchManager.cs b/MediaBrowser.Controller/Library/ISearchManager.cs
new file mode 100644
index 0000000000..4f763829a7
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ISearchManager.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Search;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Orchestrates search operations across registered search providers.
+/// </summary>
+public interface ISearchManager
+{
+ /// <summary>
+ /// Searches for items and returns hints suitable for autocomplete/typeahead UI.
+ /// Results are ordered by relevance score from search providers.
+ /// </summary>
+ /// <param name="query">The search query including filters and pagination.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>Paginated search hints with item metadata for display.</returns>
+ Task<QueryResult<SearchHintInfo>> GetSearchHintsAsync(
+ SearchQuery query,
+ CancellationToken cancellationToken = default);
+
+ /// <summary>
+ /// Gets ranked search results from registered providers. Returns only item IDs and
+ /// relevance scores; callers are responsible for loading items and applying user-access filtering.
+ /// </summary>
+ /// <param name="query">The search provider query with type/media filters.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>Search results containing item IDs and relevance scores.</returns>
+ Task<IReadOnlyList<SearchResult>> GetSearchResultsAsync(
+ SearchProviderQuery query,
+ CancellationToken cancellationToken = default);
+
+ /// <summary>
+ /// Registers search providers discovered through dependency injection.
+ /// Called during application startup.
+ /// </summary>
+ /// <param name="providers">The search providers to register.</param>
+ void AddParts(IEnumerable<ISearchProvider> providers);
+
+ /// <summary>
+ /// Gets all registered search providers ordered by priority.
+ /// </summary>
+ /// <returns>The list of search providers including the SQL fallback provider.</returns>
+ IReadOnlyList<ISearchProvider> GetProviders();
+}
diff --git a/MediaBrowser.Controller/Library/ISearchProvider.cs b/MediaBrowser.Controller/Library/ISearchProvider.cs
new file mode 100644
index 0000000000..3b300ed38b
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ISearchProvider.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Interface for search providers.
+/// </summary>
+public interface ISearchProvider
+{
+ /// <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 priority of the provider. Lower values execute first.
+ /// </summary>
+ int Priority { get; }
+
+ /// <summary>
+ /// Searches for items matching the query.
+ /// </summary>
+ /// <param name="query">The search query.</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>Ranked list of candidate item IDs with scores.</returns>
+ Task<IReadOnlyList<SearchResult>> SearchAsync(
+ SearchProviderQuery query,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Determines whether this provider can handle the given query.
+ /// </summary>
+ /// <param name="query">The search query to evaluate.</param>
+ /// <returns>True if this provider can search for the query; otherwise, false.</returns>
+ bool CanSearch(SearchProviderQuery query);
+}
diff --git a/MediaBrowser.Controller/Library/ISimilarItemsManager.cs b/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
index 0ced6f71ee..36fa547eeb 100644
--- a/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
+++ b/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
@@ -6,6 +6,7 @@ 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;
@@ -47,4 +48,23 @@ public interface ISimilarItemsManager
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/SearchProviderQuery.cs b/MediaBrowser.Controller/Library/SearchProviderQuery.cs
new file mode 100644
index 0000000000..845588c872
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SearchProviderQuery.cs
@@ -0,0 +1,45 @@
+using System;
+using Jellyfin.Data.Enums;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Query object for search providers.
+/// </summary>
+public class SearchProviderQuery
+{
+ /// <summary>
+ /// Gets the search term.
+ /// </summary>
+ public required string SearchTerm { get; init; }
+
+ /// <summary>
+ /// Gets the user ID for user-specific searches.
+ /// </summary>
+ public Guid? UserId { get; init; }
+
+ /// <summary>
+ /// Gets the item types to include in the search.
+ /// </summary>
+ public BaseItemKind[] IncludeItemTypes { get; init; } = [];
+
+ /// <summary>
+ /// Gets the item types to exclude from the search.
+ /// </summary>
+ public BaseItemKind[] ExcludeItemTypes { get; init; } = [];
+
+ /// <summary>
+ /// Gets the media types to include in the search.
+ /// </summary>
+ public MediaType[] MediaTypes { get; init; } = [];
+
+ /// <summary>
+ /// Gets the maximum number of results to return.
+ /// </summary>
+ public int? Limit { get; init; }
+
+ /// <summary>
+ /// Gets the parent ID to scope the search.
+ /// </summary>
+ public Guid? ParentId { get; init; }
+}
diff --git a/MediaBrowser.Controller/Library/SearchResult.cs b/MediaBrowser.Controller/Library/SearchResult.cs
new file mode 100644
index 0000000000..e6f145e979
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SearchResult.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Represents an item matched by a search query with its relevance score.
+/// </summary>
+public readonly struct SearchResult : IEquatable<SearchResult>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SearchResult"/> struct.
+ /// </summary>
+ /// <param name="itemId">The item ID.</param>
+ /// <param name="score">The relevance score.</param>
+ public SearchResult(Guid itemId, float score)
+ {
+ ItemId = itemId;
+ Score = score;
+ }
+
+ /// <summary>
+ /// Gets the ID of the matching item.
+ /// </summary>
+ public Guid ItemId { get; init; }
+
+ /// <summary>
+ /// Gets the relevance score. Higher values indicate more relevant results.
+ /// </summary>
+ public float Score { get; init; }
+
+ /// <summary>
+ /// Compares two <see cref="SearchResult"/> instances for equality.
+ /// </summary>
+ /// <param name="left">The left operand.</param>
+ /// <param name="right">The right operand.</param>
+ /// <returns>True if the instances are equal; otherwise, false.</returns>
+ public static bool operator ==(SearchResult left, SearchResult right)
+ => left.Equals(right);
+
+ /// <summary>
+ /// Compares two <see cref="SearchResult"/> instances for inequality.
+ /// </summary>
+ /// <param name="left">The left operand.</param>
+ /// <param name="right">The right operand.</param>
+ /// <returns>True if the instances are not equal; otherwise, false.</returns>
+ public static bool operator !=(SearchResult left, SearchResult right)
+ => !left.Equals(right);
+
+ /// <inheritdoc/>
+ public override bool Equals(object? obj)
+ => obj is SearchResult other && Equals(other);
+
+ /// <inheritdoc/>
+ public bool Equals(SearchResult other)
+ => ItemId.Equals(other.ItemId) && Score.Equals(other.Score);
+
+ /// <inheritdoc/>
+ public override int GetHashCode()
+ => HashCode.Combine(ItemId, Score);
+}
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/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 8f6e36bce4..320e65231c 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 =
[
@@ -443,6 +444,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|| state.VideoStream.VideoRangeType == VideoRangeType.HLG);
}
+ private static bool IsDeinterlaceAvailable(EncodingJobInfo state)
+ {
+ var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+ var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
+ return doDeintH264 || doDeintHevc;
+ }
+
private bool IsVideoStreamHevcRext(EncodingJobInfo state)
{
var videoStream = state.VideoStream;
@@ -1547,20 +1555,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)
@@ -2014,11 +2063,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;
@@ -3002,23 +3055,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;
@@ -3821,9 +3857,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isV4l2Encoder = vidEncoder.Contains("h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
- 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 doDeintH2645 = IsDeinterlaceAvailable(state);
var doToneMap = IsSwTonemapAvailable(state, options);
var requireDoviReshaping = doToneMap && state.VideoStream.VideoRangeType == VideoRangeType.DOVI;
@@ -3975,9 +4009,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isCuInCuOut = isNvDecoder && isNvencEncoder;
var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30;
- 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 doDeintH2645 = IsDeinterlaceAvailable(state);
var doCuTonemap = IsHwTonemapAvailable(state, options);
var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
@@ -4186,9 +4218,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isDxInDxOut = isD3d11vaDecoder && isAmfEncoder;
- 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 doDeintH2645 = IsDeinterlaceAvailable(state);
var doOclTonemap = IsHwTonemapAvailable(state, options);
var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
@@ -4434,9 +4464,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isQsvInQsvOut = isHwDecoder && isQsvEncoder;
- 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 doDeintH2645 = IsDeinterlaceAvailable(state);
var doVppTonemap = IsIntelVppTonemapAvailable(state, options);
var doOclTonemap = !doVppTonemap && IsHwTonemapAvailable(state, options);
var doTonemap = doVppTonemap || doOclTonemap;
@@ -4728,12 +4756,10 @@ namespace MediaBrowser.Controller.MediaEncoding
var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isQsvInQsvOut = isHwDecoder && isQsvEncoder;
- var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
- var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doVaVppTonemap = IsIntelVppTonemapAvailable(state, options);
var doOclTonemap = !doVaVppTonemap && IsHwTonemapAvailable(state, options);
var doTonemap = doVaVppTonemap || doOclTonemap;
- var doDeintH2645 = doDeintH264 || doDeintHevc;
+ var doDeintH2645 = IsDeinterlaceAvailable(state);
var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
@@ -5059,12 +5085,10 @@ namespace MediaBrowser.Controller.MediaEncoding
var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var isVaInVaOut = isVaapiDecoder && isVaapiEncoder;
- var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
- var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doVaVppTonemap = isVaapiDecoder && IsIntelVppTonemapAvailable(state, options);
var doOclTonemap = !doVaVppTonemap && IsHwTonemapAvailable(state, options);
var doTonemap = doVaVppTonemap || doOclTonemap;
- var doDeintH2645 = doDeintH264 || doDeintHevc;
+ var doDeintH2645 = IsDeinterlaceAvailable(state);
var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
@@ -5296,10 +5320,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var isSwEncoder = !isVaapiEncoder;
var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
- var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
- var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
var doVkTonemap = IsVulkanHwTonemapAvailable(state, options);
- var doDeintH2645 = doDeintH264 || doDeintHevc;
+ var doDeintH2645 = IsDeinterlaceAvailable(state);
var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
@@ -5536,9 +5558,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isi965Driver = _mediaEncoder.IsVaapiDeviceInteli965;
var isAmdDriver = _mediaEncoder.IsVaapiDeviceAmd;
- 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 doDeintH2645 = IsDeinterlaceAvailable(state);
var doOclTonemap = IsHwTonemapAvailable(state, options);
var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
@@ -5769,9 +5789,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var reqMaxH = state.BaseRequest.MaxHeight;
var threeDFormat = state.MediaSource.Video3DFormat;
- 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 doDeintH2645 = IsDeinterlaceAvailable(state);
var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options);
var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options);
var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface);
@@ -5970,9 +5988,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& (vidEncoder.Contains("h264", StringComparison.OrdinalIgnoreCase)
|| vidEncoder.Contains("hevc", StringComparison.OrdinalIgnoreCase));
- 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 doDeintH2645 = IsDeinterlaceAvailable(state);
var doOclTonemap = IsHwTonemapAvailable(state, options);
var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
@@ -6236,12 +6252,21 @@ namespace MediaBrowser.Controller.MediaEncoding
overlayFilters?.RemoveAll(string.IsNullOrEmpty);
var framerate = GetFramerateParam(state);
- if (framerate.HasValue)
+ if (mainFilters is not null && framerate.HasValue)
{
- mainFilters.Insert(0, string.Format(
- CultureInfo.InvariantCulture,
- "fps={0}",
- framerate.Value));
+ var doDeintH2645 = IsDeinterlaceAvailable(state);
+ var fpsFilter = string.Format(CultureInfo.InvariantCulture, "fps={0}", framerate.Value);
+
+ // For filter chain containing the deinterlace filter,
+ // place the fps filter at the end to preserve temporal info.
+ if (doDeintH2645)
+ {
+ mainFilters.Add(fpsFilter);
+ }
+ else
+ {
+ mainFilters.Insert(0, fpsFilter);
+ }
}
var mainStr = string.Empty;
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/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/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>