diff options
Diffstat (limited to 'MediaBrowser.Controller')
6 files changed, 143 insertions, 22 deletions
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 f4c2196400..c23eba75ef 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -598,6 +598,15 @@ namespace MediaBrowser.Controller.Library IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query); /// <summary> + /// Gets distinct people names for multiple items. + /// </summary> + /// <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. /// </summary> /// <param name="query">The query.</param> 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/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 8688ea4b6c..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 = [ @@ -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) @@ -3006,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; diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs index a89f3ef9ee..7474130ec4 100644 --- a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs +++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs @@ -32,4 +32,13 @@ 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 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">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); } |
