Merge remote-tracking branch 'upstream/master' into epg-fixes

This commit is contained in:
Shadowghost
2026-04-11 17:48:00 +02:00
119 changed files with 2160 additions and 1049 deletions

View File

@@ -154,11 +154,6 @@ namespace MediaBrowser.Controller.Entities.Audio
return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
}
protected override bool GetBlockUnratedValue(User user)
{
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()
{
return UnratedItem.Music;

View File

@@ -22,7 +22,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
@@ -1172,11 +1171,18 @@ namespace MediaBrowser.Controller.Entities
info.Video3DFormat = video.Video3DFormat;
info.Timestamp = video.Timestamp;
if (video.IsShortcut)
if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
{
info.IsRemote = true;
info.Path = video.ShortcutPath;
info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
// Only allow remote shortcut paths — local file paths in .strm files
// could be used to read arbitrary files from the server.
if (shortcutProtocol != MediaProtocol.File)
{
info.IsRemote = true;
info.Path = video.ShortcutPath;
info.Protocol = shortcutProtocol;
}
}
if (string.IsNullOrEmpty(info.Container))
@@ -1601,11 +1607,10 @@ namespace MediaBrowser.Controller.Entities
if (string.IsNullOrEmpty(rating))
{
Logger.LogDebug("{0} has no parental rating set.", Name);
return !GetBlockUnratedValue(user);
}
var ratingScore = LocalizationManager.GetRatingScore(rating);
var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
// Could not determine rating level
if (ratingScore is null)
@@ -1647,7 +1652,7 @@ namespace MediaBrowser.Controller.Entities
return null;
}
return LocalizationManager.GetRatingScore(rating);
return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode());
}
public List<string> GetInheritedTags()
@@ -2129,17 +2134,6 @@ namespace MediaBrowser.Controller.Entities
};
}
// Music albums usually don't have dedicated backdrops, so return one from the artist instead
if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop)
{
var artist = FindParent<MusicArtist>();
if (artist is not null)
{
return artist.GetImages(imageType).ElementAtOrDefault(imageIndex);
}
}
return GetImages(imageType)
.ElementAtOrDefault(imageIndex);
}
@@ -2621,7 +2615,7 @@ namespace MediaBrowser.Controller.Entities
.Select(i => i.OfficialRating)
.Where(i => !string.IsNullOrEmpty(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(rating => (rating, LocalizationManager.GetRatingScore(rating)))
.Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode())))
.OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
.ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
.Select(i => i.rating);

View File

@@ -10,6 +10,7 @@ using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Entities
{
@@ -388,5 +389,75 @@ namespace MediaBrowser.Controller.Entities
User = user;
}
public void ApplyFilters(ItemFilter[] filters)
{
static void ThrowConflictingFilters()
=> throw new ArgumentException("Conflicting filters", nameof(filters));
foreach (var filter in filters)
{
switch (filter)
{
case ItemFilter.IsFolder:
if (filters.Contains(ItemFilter.IsNotFolder))
{
ThrowConflictingFilters();
}
IsFolder = true;
break;
case ItemFilter.IsNotFolder:
if (filters.Contains(ItemFilter.IsFolder))
{
ThrowConflictingFilters();
}
IsFolder = false;
break;
case ItemFilter.IsUnplayed:
if (filters.Contains(ItemFilter.IsPlayed))
{
ThrowConflictingFilters();
}
IsPlayed = false;
break;
case ItemFilter.IsPlayed:
if (filters.Contains(ItemFilter.IsUnplayed))
{
ThrowConflictingFilters();
}
IsPlayed = true;
break;
case ItemFilter.IsFavorite:
IsFavorite = true;
break;
case ItemFilter.IsResumable:
IsResumable = true;
break;
case ItemFilter.Likes:
if (filters.Contains(ItemFilter.Dislikes))
{
ThrowConflictingFilters();
}
IsLiked = true;
break;
case ItemFilter.Dislikes:
if (filters.Contains(ItemFilter.Likes))
{
ThrowConflictingFilters();
}
IsLiked = false;
break;
case ItemFilter.IsFavoriteOrLikes:
IsFavoriteOrLiked = true;
break;
}
}
}
}
}

View File

@@ -21,6 +21,8 @@ namespace MediaBrowser.Controller.Entities
ExcludePersonTypes = excludePersonTypes;
}
public int? StartIndex { get; set; }
/// <summary>
/// Gets or sets the maximum number of items the query should return.
/// </summary>
@@ -28,6 +30,8 @@ namespace MediaBrowser.Controller.Entities
public Guid ItemId { get; set; }
public Guid? ParentId { get; set; }
public IReadOnlyList<string> PersonTypes { get; }
public IReadOnlyList<string> ExcludePersonTypes { get; }

View File

@@ -201,12 +201,17 @@ namespace MediaBrowser.Controller.Entities.TV
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
if (series is null)
{
return [];
}
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
}
public List<BaseItem> GetEpisodes()
{
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
return GetEpisodes(Series, null, null, new DtoOptions(true), true);
}
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)

View File

@@ -33,18 +33,18 @@ namespace MediaBrowser.Controller.MediaEncoding
public partial class EncodingHelper
{
/// <summary>
/// The codec validation regex.
/// The codec validation regex string.
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
/// This should matches all common valid codecs.
/// </summary>
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
/// <summary>
/// The level validation regex.
/// The level validation regex string.
/// This regular expression matches strings representing a double.
/// </summary>
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
public const string LevelValidationRegexStr = @"-?[0-9]+(?:\.[0-9]+)?";
private const string _defaultMjpegEncoder = "mjpeg";
@@ -85,8 +85,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0);
private static readonly string[] _videoProfilesH264 =
[
@@ -180,6 +179,22 @@ namespace MediaBrowser.Controller.MediaEncoding
RemoveHdr10Plus,
}
/// <summary>
/// The codec validation regex.
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
/// This should matches all common valid codecs.
/// </summary>
[GeneratedRegex(ContainerValidationRegexStr)]
public static partial Regex ContainerValidationRegex();
/// <summary>
/// The level validation regex string.
/// This regular expression matches strings representing a double.
/// </summary>
[GeneratedRegex(LevelValidationRegexStr)]
public static partial Regex LevelValidationRegex();
[GeneratedRegex(@"\s+")]
private static partial Regex WhiteSpaceRegex();
@@ -476,7 +491,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetMjpegEncoder(state, encodingOptions);
}
if (_containerValidationRegex.IsMatch(codec))
if (ContainerValidationRegex().IsMatch(codec))
{
return codec.ToLowerInvariant();
}
@@ -517,7 +532,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetInputFormat(string container)
{
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
if (string.IsNullOrEmpty(container) || !ContainerValidationRegex().IsMatch(container))
{
return null;
}
@@ -735,7 +750,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = state.OutputAudioCodec;
if (!_containerValidationRegex.IsMatch(codec))
if (!ContainerValidationRegex().IsMatch(codec))
{
codec = "aac";
}
@@ -1267,6 +1282,20 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
// Use analyzeduration also for subtitle streams to improve resolution detection with streams inside MKS files
var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state);
if (!string.IsNullOrEmpty(analyzeDurationArgument))
{
arg.Append(' ').Append(analyzeDurationArgument);
}
// Apply probesize, too, if configured
var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg();
if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument))
{
arg.Append(' ').Append(ffmpegProbeSizeArgument);
}
// Also seek the external subtitles stream.
var seekSubParam = GetFastSeekCommandLineParameter(state, options, segmentContainer);
if (!string.IsNullOrEmpty(seekSubParam))
@@ -1552,14 +1581,15 @@ namespace MediaBrowser.Controller.MediaEncoding
int bitrate = state.OutputVideoBitrate.Value;
// Bit rate under 1000k is not allowed in h264_qsv
// Bit rate under 1000k is not allowed in h264_qsv.
if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{
bitrate = Math.Max(bitrate, 1000);
}
// Currently use the same buffer size for all encoders
int bufsize = bitrate * 2;
// Currently use the same buffer size for all non-QSV encoders.
// Use long arithmetic to prevent int32 overflow for very high bitrate values.
int bufsize = (int)Math.Min((long)bitrate * 2, int.MaxValue);
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
{
@@ -1589,7 +1619,13 @@ namespace MediaBrowser.Controller.MediaEncoding
// Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation
// Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}");
// Use long arithmetic and clamp to int.MaxValue to prevent int32 overflow
// (e.g. bitrate * 4 wraps to a negative value for bitrates above ~537 million)
int qsvMaxrate = (int)Math.Min((long)bitrate + 1, int.MaxValue);
int qsvInitOcc = (int)Math.Min((long)bitrate * 2, int.MaxValue);
int qsvBufsize = (int)Math.Min((long)bitrate * 4, int.MaxValue);
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {qsvMaxrate} -rc_init_occupancy {qsvInitOcc} -bufsize {qsvBufsize}");
}
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
@@ -1768,38 +1804,40 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
if (!double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
{
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
return null;
}
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.3 (15) and lower for maximum compatibility.
// https://en.wikipedia.org/wiki/AV1#Levels
if (requestLevel < 0 || requestLevel >= 15)
{
// Transcode to level 5.3 (15) and lower for maximum compatibility.
// https://en.wikipedia.org/wiki/AV1#Levels
if (requestLevel < 0 || requestLevel >= 15)
{
return "15";
}
return "15";
}
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
}
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.0 and lower for maximum compatibility.
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
if (requestLevel < 0 || requestLevel >= 150)
{
// Transcode to level 5.0 and lower for maximum compatibility.
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
if (requestLevel < 0 || requestLevel >= 150)
{
return "150";
}
return "150";
}
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
}
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.1 and lower for maximum compatibility.
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
if (requestLevel < 0 || requestLevel >= 51)
{
// Transcode to level 5.1 and lower for maximum compatibility.
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
if (requestLevel < 0 || requestLevel >= 51)
{
return "51";
}
return "51";
}
}
@@ -2189,12 +2227,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
var level = state.GetRequestedLevel(targetVideoCodec);
var level = NormalizeTranscodingLevel(state, state.GetRequestedLevel(targetVideoCodec));
if (!string.IsNullOrEmpty(level))
{
level = NormalizeTranscodingLevel(state, level);
// libx264, QSV, AMF can adjust the given level to match the output.
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
@@ -2592,8 +2628,16 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
// Cap the max target bitrate to intMax/2 to satisfy the bufsize=bitrate*2.
return Math.Min(bitrate ?? 0, int.MaxValue / 2);
// Cap the max target bitrate to 400 Mbps.
// No consumer or professional hardware transcode target exceeds this value
// (Intel QSV tops out at ~300 Mbps for H.264; HEVC High Tier Level 5.x is ~240 Mbps).
// Without this cap, plugin-provided MPEG-TS streams with no usable bitrate metadata
// can produce unreasonably large -bufsize/-maxrate values for the encoder.
// Note: the existing FallbackMaxStreamingBitrate mechanism (default 30 Mbps) only
// applies when a LiveStreamId is set (M3U/HDHR sources). Plugin streams and other
// sources that bypass the LiveTV pipeline are not covered by it.
const int MaxSaneBitrate = 400_000_000; // 400 Mbps
return Math.Min(bitrate ?? 0, MaxSaneBitrate);
}
private int GetMinBitrate(int sourceBitrate, int requestedBitrate)
@@ -6359,17 +6403,15 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
&& ((videoStream.Profile?.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) ?? false)
|| (videoStream.Profile?.Contains("4:4:4", StringComparison.OrdinalIgnoreCase) ?? false)))
{
if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase)
|| videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase))
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
{
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
{
return null;
}
return null;
}
}
@@ -7123,9 +7165,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
private string GetFfmpegAnalyzeDurationArg(EncodingJobInfo state)
{
var inputModifier = string.Empty;
var analyzeDurationArgument = string.Empty;
// Apply -analyzeduration as per the environment variable,
@@ -7141,6 +7182,26 @@ namespace MediaBrowser.Controller.MediaEncoding
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
}
return analyzeDurationArgument;
}
private string GetFfmpegProbesizeArg()
{
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
if (!string.IsNullOrEmpty(ffmpegProbeSize))
{
return $"-probesize {ffmpegProbeSize}";
}
return string.Empty;
}
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
{
var inputModifier = string.Empty;
var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state);
if (!string.IsNullOrEmpty(analyzeDurationArgument))
{
inputModifier += " " + analyzeDurationArgument;
@@ -7149,11 +7210,11 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier = inputModifier.Trim();
// Apply -probesize if configured
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg();
if (!string.IsNullOrEmpty(ffmpegProbeSize))
if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument))
{
inputModifier += $" -probesize {ffmpegProbeSize}";
inputModifier += " " + ffmpegProbeSizeArgument;
}
var userAgentParam = GetUserAgentParam(state);
@@ -7193,8 +7254,10 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
}
int readrate = 0;
if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp)
{
readrate = 1;
inputModifier += " -re";
}
else if (encodingOptions.EnableSegmentDeletion
@@ -7205,7 +7268,15 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// Set an input read rate limit 10x for using SegmentDeletion with stream-copy
// to prevent ffmpeg from exiting prematurely (due to fast drive)
inputModifier += " -readrate 10";
readrate = 10;
inputModifier += $" -readrate {readrate}";
}
// Set a larger catchup value to revert to the old behavior,
// otherwise, remuxing might stall due to this new option
if (readrate > 0 && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateCatchupOption)
{
inputModifier += $" -readrate_catchup {readrate * 100}";
}
var flags = new List<string>();

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@@ -31,4 +32,13 @@ public interface IMediaSegmentProvider
/// <param name="item">The base item to extract segments from.</param>
/// <returns>True if item is supported, otherwise false.</returns>
ValueTask<bool> Supports(BaseItem item);
/// <summary>
/// Called when extracted segment data for an item is being pruned.
/// Providers should delete any cached analysis data they hold for the given item.
/// </summary>
/// <param name="itemId">The item whose data is being pruned.</param>
/// <param name="cancellationToken">Abort token.</param>
/// <returns>A task representing the asynchronous cleanup operation.</returns>
Task CleanupExtractedData(Guid itemId, CancellationToken cancellationToken);
}