Merge remote-tracking branch 'upstream/master' into search-rebased

# Conflicts:
#	Emby.Server.Implementations/Library/LibraryManager.cs
#	Jellyfin.Server.Implementations/Item/PeopleRepository.cs
#	MediaBrowser.Controller/Library/ILibraryManager.cs
#	MediaBrowser.Controller/Persistence/IPeopleRepository.cs
This commit is contained in:
Shadowghost
2026-05-30 19:07:18 +02:00
56 changed files with 5203 additions and 558 deletions

View File

@@ -57,6 +57,14 @@ namespace MediaBrowser.Controller.Collections
/// <returns>IEnumerable{BaseItem}.</returns>
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>

View File

@@ -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)

View File

@@ -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();

View File

@@ -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)

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -598,12 +598,13 @@ namespace MediaBrowser.Controller.Library
IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query);
/// <summary>
/// Gets the people names per item for a batch of item IDs in a single DB round-trip.
/// Gets distinct people names for multiple items.
/// </summary>
/// <param name="itemIds">The item IDs to look up.</param>
/// <param name="personTypes">Optional person types to include. Empty for all.</param>
/// <returns>Dictionary keyed by item id; values are the per-item people names. Items with no people are absent.</returns>
IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItem(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes);
/// <param name="itemIds">The item IDs.</param>
/// <param name="personTypes">The person types to include.</param>
/// <param name="limit">Maximum number of names.</param>
/// <returns>The distinct people names.</returns>
IReadOnlyList<string> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes, int limit);
/// <summary>
/// Queries the items.

View File

@@ -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);
}

View File

@@ -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; }
}

View File

@@ -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 =
[
@@ -1547,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)
@@ -2014,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;
@@ -3002,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;

View File

@@ -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.

View File

@@ -34,12 +34,11 @@ public interface IPeopleRepository
IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter);
/// <summary>
/// Gets the people names per item for a batch of item IDs, preserving per-item list order.
/// One database round-trip for the whole batch; grouped by item id in memory.
/// Items with no people are omitted from the returned dictionary.
/// Gets distinct people names 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">Optional person types to include (e.g. "Actor", "Director"). Empty for all.</param>
/// <returns>Dictionary keyed by item id; values are the per-item people names.</returns>
IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItem(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes);
/// <param name="personTypes">The person types to include (e.g. "Actor", "Director").</param>
/// <param name="limit">Maximum number of names to return.</param>
/// <returns>The distinct people names.</returns>
IReadOnlyList<string> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes, int limit);
}