From 0874a26131a5a1e9d62fb7231acbaf9eb921b5c8 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 9 Jun 2026 23:23:03 +0200 Subject: Coalesce alternate-version progress onto primary in resume filter --- MediaBrowser.Model/Dto/MediaSourceInfo.cs | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 75ccdcf276..017e26ef59 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -55,6 +55,11 @@ namespace MediaBrowser.Model.Dto public long? RunTimeTicks { get; set; } + /// + /// Gets or sets the playback position for this specific source. + /// + public long? PlaybackPositionTicks { get; set; } + public bool ReadAtNativeFramerate { get; set; } public bool IgnoreDts { get; set; } -- cgit v1.2.3 From 1dd5a850807bc4269fce66d2d4191e67027285c8 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 15 Jun 2026 09:29:24 +0200 Subject: Extend TranscodingReason reporting --- .../MediaEncoding/EncodingHelper.cs | 77 ++++++++++++++-------- .../MediaEncoding/EncodingJobInfo.cs | 9 +++ MediaBrowser.Model/Dlna/StreamBuilder.cs | 8 +++ 3 files changed, 65 insertions(+), 29 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 320e65231c..650eaa404e 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -25,6 +25,7 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Configuration; using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; @@ -2611,56 +2612,66 @@ namespace MediaBrowser.Controller.MediaEncoding } public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable supportedAudioCodecs) + => CanStreamCopyAudio(state, audioStream, supportedAudioCodecs, out _); + + /// + /// Determines whether the given audio stream can be stream-copied and, regardless of the outcome, + /// reports the codec/parameter incompatibilities that would force a re-encode via . + /// + /// The encoding job state. + /// The source audio stream. + /// The audio codecs the target supports. + /// The codec/parameter incompatibilities preventing a copy, or 0 if the stream is copy-compatible. + /// true if the audio stream can be stream-copied; otherwise, false. + public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable supportedAudioCodecs, out TranscodeReason failureReasons) { var request = state.BaseRequest; - if (!request.AllowAudioStreamCopy) - { - return false; - } + // Policy-independent compatibility check, so the reasons are reported even when a policy gate is what ultimately prevents the copy. + failureReasons = GetAudioStreamCopyFailureReasons(state, audioStream, supportedAudioCodecs); + + return request.AllowAudioStreamCopy + && request.EnableAutoStreamCopy + && failureReasons == 0; + } + + private static TranscodeReason GetAudioStreamCopyFailureReasons(EncodingJobInfo state, MediaStream audioStream, IEnumerable supportedAudioCodecs) + { + var request = state.BaseRequest; + TranscodeReason reasons = 0; var maxBitDepth = state.GetRequestedAudioBitDepth(audioStream.Codec); if (maxBitDepth.HasValue && audioStream.BitDepth.HasValue && audioStream.BitDepth.Value > maxBitDepth.Value) { - return false; + reasons |= TranscodeReason.AudioBitDepthNotSupported; } // Source and target codecs must match if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparison.OrdinalIgnoreCase)) { - return false; + reasons |= TranscodeReason.AudioCodecNotSupported; } // Channels must fall within requested value var channels = state.GetRequestedAudioChannels(audioStream.Codec); - if (channels.HasValue) + if (channels.HasValue + && (!audioStream.Channels.HasValue + || audioStream.Channels.Value <= 0 + || audioStream.Channels.Value > channels.Value)) { - if (!audioStream.Channels.HasValue || audioStream.Channels.Value <= 0) - { - return false; - } - - if (audioStream.Channels.Value > channels.Value) - { - return false; - } + reasons |= TranscodeReason.AudioChannelsNotSupported; } // Sample rate must fall within requested value - if (request.AudioSampleRate.HasValue) + if (request.AudioSampleRate.HasValue + && (!audioStream.SampleRate.HasValue + || audioStream.SampleRate.Value <= 0 + || audioStream.SampleRate.Value > request.AudioSampleRate.Value)) { - if (!audioStream.SampleRate.HasValue || audioStream.SampleRate.Value <= 0) - { - return false; - } - - if (audioStream.SampleRate.Value > request.AudioSampleRate.Value) - { - return false; - } + reasons |= TranscodeReason.AudioSampleRateNotSupported; } // Audio bitrate must fall within requested value @@ -2668,10 +2679,10 @@ namespace MediaBrowser.Controller.MediaEncoding && audioStream.BitRate.HasValue && audioStream.BitRate.Value > request.AudioBitRate.Value) { - return false; + reasons |= TranscodeReason.AudioBitrateNotSupported; } - return request.EnableAutoStreamCopy; + return reasons; } public int GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec) @@ -7217,8 +7228,9 @@ namespace MediaBrowser.Controller.MediaEncoding && !IsCopyCodec(state.OutputVideoCodec) && options.HlsAudioSeekStrategy is HlsAudioSeekStrategy.TranscodeAudio; + TranscodeReason audioCopyFailureReasons = 0; if (state.AudioStream is not null - && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs) + && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs, out audioCopyFailureReasons) && !preventHlsAudioCopy) { state.OutputAudioCodec = "copy"; @@ -7232,6 +7244,13 @@ namespace MediaBrowser.Controller.MediaEncoding { state.OutputAudioCodec = "copy"; } + else if (state.AudioStream is not null && !IsCopyCodec(state.OutputAudioCodec)) + { + // Audio is actually being re-encoded although the playback determination may have considered the source copyable. + // Only carry the primary "cannot be passed through" cause - the codec mismatch. + // Bitrate/channels/sample-rate/bit-depth copy refusals are consequences of the chosen transcode target. + state.AddTranscodeReason(audioCopyFailureReasons & TranscodeReason.AudioCodecNotSupported); + } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 3a1897a244..314cd32903 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -515,6 +515,15 @@ namespace MediaBrowser.Controller.MediaEncoding public int HlsListSize => 0; + /// + /// Adds the specified reason(s) to . + /// + /// The transcode reason(s) to add. + public void AddTranscodeReason(TranscodeReason reason) + { + _transcodeReasons = TranscodeReasons | reason; + } + private int? GetMediaStreamCount(MediaStreamType type, int limit) { var count = MediaSource.GetStreamCount(type); diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index d875bbe8ed..59f97d8c7c 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -951,6 +951,10 @@ namespace MediaBrowser.Model.Dlna } playlistItem.VideoCodecs = videoCodecs; + if (videoStream is not null && !ContainerHelper.ContainsContainer(videoCodecs, false, videoStream.Codec)) + { + playlistItem.TranscodeReasons |= TranscodeReason.VideoCodecNotSupported; + } // Copy video codec options as a starting point, this applies to transcode and direct-stream playlistItem.MaxFramerate = videoStream?.ReferenceFrameRate; @@ -999,6 +1003,10 @@ namespace MediaBrowser.Model.Dlna var directAudioFailures = audioStreamWithSupportedCodec is null ? default : GetCompatibilityAudioCodec(options, item, container ?? string.Empty, audioStreamWithSupportedCodec, null, true, false); playlistItem.TranscodeReasons |= directAudioFailures; + if (audioStream is not null && audioStreamWithSupportedCodec is null) + { + playlistItem.TranscodeReasons |= TranscodeReason.AudioCodecNotSupported; + } var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit && directAudioFailures == 0; -- cgit v1.2.3 From 8622c3bfb78075ef78b2e89f0cf519e130b062c4 Mon Sep 17 00:00:00 2001 From: altqx Date: Fri, 3 Jul 2026 09:30:22 +0700 Subject: Match VobSub MKS subtitle profiles by container --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 30 ++++++++++------ .../Dlna/StreamBuilderTests.cs | 42 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 59f97d8c7c..a9ab7d6db0 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -576,11 +576,8 @@ namespace MediaBrowser.Model.Dlna foreach (var profile in subtitleProfiles) { if (profile.Method == SubtitleDeliveryMethod.External - && (string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase) - // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are exposed as .mks. - || (string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase) - && stream.IsVobSubSubtitleStream - && (!stream.IsExternal || stream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))))) + && (IsVobSubMksProfile(profile, stream) + || (!IsVobSubMksDeliveryProfile(profile) && string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase)))) { return stream.Index; } @@ -1590,13 +1587,11 @@ namespace MediaBrowser.Model.Dlna continue; } - // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are matched against external .mks delivery profiles. - bool isVobSubMksProfile = string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase) - && subtitleStream.IsVobSubSubtitleStream - && (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)); + bool isVobSubMksProfile = IsVobSubMksProfile(profile, subtitleStream); if ((profile.Method == SubtitleDeliveryMethod.External - && (isVobSubMksProfile || subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))) || + && (isVobSubMksProfile + || (!IsVobSubMksDeliveryProfile(profile) && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format)))) || (profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream)) { bool requiresConversion = !isVobSubMksProfile @@ -1628,6 +1623,21 @@ namespace MediaBrowser.Model.Dlna return null; } + private static bool IsVobSubMksDeliveryProfile(SubtitleProfile profile) + { + return MediaStream.IsVobSubFormat(profile.Format) + && !string.IsNullOrWhiteSpace(profile.Container) + && ContainerHelper.ContainsContainer(profile.Container, "mks"); + } + + private static bool IsVobSubMksProfile(SubtitleProfile profile, MediaStream subtitleStream) + { + // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are exposed as .mks. + return IsVobSubMksDeliveryProfile(profile) + && subtitleStream.IsVobSubSubtitleStream + && (!subtitleStream.IsExternal || subtitleStream.Path?.EndsWith(".mks", StringComparison.OrdinalIgnoreCase) == true); + } + private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate) { // Don't restrict bitrate if item is remote. diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index d94d56bc20..5ba061296a 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -676,6 +676,48 @@ namespace Jellyfin.Model.Tests Assert.Equal(expectedMethod, result.Method); } + [Theory] + [InlineData(false, null, true, SubtitleDeliveryMethod.External)] + [InlineData(false, null, false, SubtitleDeliveryMethod.Encode)] + [InlineData(true, "/media/sub.mks", true, SubtitleDeliveryMethod.External)] + [InlineData(true, "/media/sub.idx", true, SubtitleDeliveryMethod.Encode)] + [InlineData(true, "/media/sub.sub", true, SubtitleDeliveryMethod.Encode)] + public void GetSubtitleProfile_MatchesVobSubMksProfileOnlyWhenDeliveredAsMks( + bool isExternal, + string? path, + bool enableSubtitleExtraction, + SubtitleDeliveryMethod expectedMethod) + { + var mediaSource = new MediaSourceInfo(); + var subtitleStream = new MediaStream + { + Type = MediaStreamType.Subtitle, + Index = 0, + IsExternal = isExternal, + Path = path, + Codec = "vobsub" + }; + + var subtitleProfiles = new[] + { + new SubtitleProfile { Format = "vobsub", Container = "mks", Method = SubtitleDeliveryMethod.External } + }; + + var transcoderSupport = new Mock(); + transcoderSupport.Setup(t => t.CanExtractSubtitles(It.IsAny())).Returns(enableSubtitleExtraction); + + var result = StreamBuilder.GetSubtitleProfile( + mediaSource, + subtitleStream, + subtitleProfiles, + PlayMethod.Transcode, + transcoderSupport.Object, + null, + null); + + Assert.Equal(expectedMethod, result.Method); + } + [Theory] // External text subs embedded into MKV when transcoding (#16403) [InlineData("srt", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] -- cgit v1.2.3 From b8bac71270f7b2fccbbdda1f28e1f50a554c7383 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 14 Jul 2026 10:05:32 +0200 Subject: remove PlaybackPositionTicks from MediaSourceInfo --- .../Library/MediaSourceManager.cs | 18 ++++-------------- MediaBrowser.Model/Dto/MediaSourceInfo.cs | 5 ----- .../Library/MediaSourceManagerTests.cs | 12 +++--------- 3 files changed, 7 insertions(+), 28 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c64833ddaa..97e00177b6 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -418,10 +418,10 @@ namespace Emby.Server.Implementations.Library } /// - /// Populates each source's own playback position for the user and, when the queried item is a - /// primary, moves the most recently played version to the front so that resuming without an - /// explicit source selection plays the version that was last watched. A directly queried - /// alternate version keeps its own source first. + /// When the queried item is a primary, moves the most recently played version to the front so + /// that resuming without an explicit source selection plays the version that was last watched. + /// A directly queried alternate version keeps its own source first. Per-user playback position + /// is not surfaced on the source itself; it is carried by each version's own UserData. /// /// The queried item. /// The item's media sources. @@ -451,16 +451,6 @@ namespace Emby.Server.Implementations.Library } } - foreach (var source in sources) - { - if (source.Id is not null - && dataBySourceId.TryGetValue(source.Id, out var data) - && data.PlaybackPositionTicks > 0) - { - source.PlaybackPositionTicks = data.PlaybackPositionTicks; - } - } - // Reorder only for a resumable (in-progress) version; // a completed version has no position to resume, so it must not be pulled to the front here. var resumeSource = VersionPlaybackSelector.SelectMostRecentlyPlayed( diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 017e26ef59..75ccdcf276 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -55,11 +55,6 @@ namespace MediaBrowser.Model.Dto public long? RunTimeTicks { get; set; } - /// - /// Gets or sets the playback position for this specific source. - /// - public long? PlaybackPositionTicks { get; set; } - public bool ReadAtNativeFramerate { get; set; } public bool IgnoreDts { get; set; } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs index b788fb304e..c80f899498 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs @@ -150,7 +150,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library } [Fact] - public void GetStaticMediaSources_PrimaryQueried_PopulatesPerVersionPositionsAndDefaultsToMostRecent() + public void GetStaticMediaSources_PrimaryQueried_DefaultsToMostRecentlyPlayedVersion() { var (primary, alt1, alt2) = SetupVersionGroup(); SetupUserDataBatch(new Dictionary @@ -161,12 +161,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library var sources = _mediaSourceManager.GetStaticMediaSources(primary, false, _user); - // Each version carries its own resume point; the primary has none. - Assert.Equal((long?)10, sources.First(s => s.Id == alt1.Id.ToString("N")).PlaybackPositionTicks); - Assert.Equal((long?)20, sources.First(s => s.Id == alt2.Id.ToString("N")).PlaybackPositionTicks); - Assert.Null(sources.First(s => s.Id == primary.Id.ToString("N")).PlaybackPositionTicks); - // The most recently played version is the default source, so resuming plays the right file. + // Per-user positions live in each version's UserData, not on the source. Assert.Equal(alt2.Id.ToString("N"), sources[0].Id); } @@ -182,9 +178,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library var sources = _mediaSourceManager.GetStaticMediaSources(alt1, false, _user); // An explicitly opened version keeps its own source first, even when a sibling was - // played more recently, but the sibling's resume point is still populated. + // played more recently. Assert.Equal(alt1.Id.ToString("N"), sources[0].Id); - Assert.Equal((long?)20, sources.First(s => s.Id == alt2.Id.ToString("N")).PlaybackPositionTicks); Assert.Equal(3, sources.Count); } @@ -197,7 +192,6 @@ namespace Jellyfin.Server.Implementations.Tests.Library var sources = _mediaSourceManager.GetStaticMediaSources(primary, false, _user); Assert.Equal(primary.Id.ToString("N"), sources[0].Id); - Assert.All(sources, s => Assert.Null(s.PlaybackPositionTicks)); } [Fact] -- cgit v1.2.3 From d96d1f111994104c7672e2f64f64a92902458f41 Mon Sep 17 00:00:00 2001 From: mbastian77 Date: Wed, 15 Jul 2026 12:11:11 +0200 Subject: Add XML docs to small DLNA model types and remove CS1591 suppressions --- MediaBrowser.Model/Dlna/CodecType.cs | 16 ++++++++++++++-- MediaBrowser.Model/Dlna/EncodingContext.cs | 12 ++++++++++-- MediaBrowser.Model/Dlna/PlaybackErrorCode.cs | 16 ++++++++++++++-- MediaBrowser.Model/Dlna/ResolutionOptions.cs | 11 +++++++++-- MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs | 12 ++++++++++-- 5 files changed, 57 insertions(+), 10 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Model/Dlna/CodecType.cs b/MediaBrowser.Model/Dlna/CodecType.cs index c9f090e4cc..12730a76fa 100644 --- a/MediaBrowser.Model/Dlna/CodecType.cs +++ b/MediaBrowser.Model/Dlna/CodecType.cs @@ -1,11 +1,23 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Dlna { + /// + /// The codec type of a codec profile. + /// public enum CodecType { + /// + /// The profile applies to a video codec. + /// Video = 0, + + /// + /// The profile applies to the audio codec of a video stream. + /// VideoAudio = 1, + + /// + /// The profile applies to an audio codec. + /// Audio = 2 } } diff --git a/MediaBrowser.Model/Dlna/EncodingContext.cs b/MediaBrowser.Model/Dlna/EncodingContext.cs index 79ca6366d7..1408333d2e 100644 --- a/MediaBrowser.Model/Dlna/EncodingContext.cs +++ b/MediaBrowser.Model/Dlna/EncodingContext.cs @@ -1,10 +1,18 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Dlna { + /// + /// The encoding context. + /// public enum EncodingContext { + /// + /// The media is transcoded on the fly and delivered as a stream. + /// Streaming = 0, + + /// + /// The media is transcoded to a static file. + /// Static = 1 } } diff --git a/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs index 300fab5c50..a28f422a2b 100644 --- a/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs +++ b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs @@ -1,11 +1,23 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Dlna { + /// + /// The playback error code. + /// public enum PlaybackErrorCode { + /// + /// Playback of the item is not allowed. + /// NotAllowed = 0, + + /// + /// No stream compatible with the device profile was found. + /// NoCompatibleStream = 1, + + /// + /// The rate limit has been exceeded. + /// RateLimitExceeded = 2 } } diff --git a/MediaBrowser.Model/Dlna/ResolutionOptions.cs b/MediaBrowser.Model/Dlna/ResolutionOptions.cs index 774592abc7..b161b4a1e4 100644 --- a/MediaBrowser.Model/Dlna/ResolutionOptions.cs +++ b/MediaBrowser.Model/Dlna/ResolutionOptions.cs @@ -1,11 +1,18 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Dlna { + /// + /// The resolution constraints. + /// public class ResolutionOptions { + /// + /// Gets or sets the maximum width. + /// public int? MaxWidth { get; set; } + /// + /// Gets or sets the maximum height. + /// public int? MaxHeight { get; set; } } } diff --git a/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs b/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs index cc0c6069bf..1563ffd17a 100644 --- a/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs +++ b/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs @@ -1,10 +1,18 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Dlna { + /// + /// The transcode seek info. + /// public enum TranscodeSeekInfo { + /// + /// The seek method is chosen automatically. + /// Auto = 0, + + /// + /// Seeking is performed by byte position. + /// Bytes = 1 } } -- cgit v1.2.3 From 5cbafa566dab227214f60924332cb0e4bfa75b32 Mon Sep 17 00:00:00 2001 From: mbastian77 Date: Wed, 15 Jul 2026 16:03:56 +0200 Subject: Add XML docs to small session model types and remove CS1591 suppressions --- MediaBrowser.Model/Session/MessageCommand.cs | 13 ++++++++++++- MediaBrowser.Model/Session/PlayMethod.cs | 16 ++++++++++++++-- MediaBrowser.Model/Session/PlaystateRequest.cs | 11 +++++++++-- MediaBrowser.Model/Session/QueueItem.cs | 10 +++++++++- MediaBrowser.Model/Session/RepeatMode.cs | 16 ++++++++++++++-- 5 files changed, 58 insertions(+), 8 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Model/Session/MessageCommand.cs b/MediaBrowser.Model/Session/MessageCommand.cs index cc9db8e6c5..e041a9cccd 100644 --- a/MediaBrowser.Model/Session/MessageCommand.cs +++ b/MediaBrowser.Model/Session/MessageCommand.cs @@ -1,17 +1,28 @@ #nullable disable -#pragma warning disable CS1591 using System.ComponentModel.DataAnnotations; namespace MediaBrowser.Model.Session { + /// + /// A command to display a message on a client. + /// public class MessageCommand { + /// + /// Gets or sets the message header. + /// public string Header { get; set; } + /// + /// Gets or sets the message text. + /// [Required(AllowEmptyStrings = false)] public string Text { get; set; } + /// + /// Gets or sets the timeout in milliseconds after which the message should be dismissed. + /// public long? TimeoutMs { get; set; } } } diff --git a/MediaBrowser.Model/Session/PlayMethod.cs b/MediaBrowser.Model/Session/PlayMethod.cs index 8067627843..2bd11cc91a 100644 --- a/MediaBrowser.Model/Session/PlayMethod.cs +++ b/MediaBrowser.Model/Session/PlayMethod.cs @@ -1,11 +1,23 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Session { + /// + /// The play method. + /// public enum PlayMethod { + /// + /// The media is transcoded before it is sent to the client. + /// Transcode = 0, + + /// + /// The media is remuxed into a compatible container but the streams are not re-encoded. + /// DirectStream = 1, + + /// + /// The media is sent to the client as-is. + /// DirectPlay = 2 } } diff --git a/MediaBrowser.Model/Session/PlaystateRequest.cs b/MediaBrowser.Model/Session/PlaystateRequest.cs index ba2c024b76..040affa144 100644 --- a/MediaBrowser.Model/Session/PlaystateRequest.cs +++ b/MediaBrowser.Model/Session/PlaystateRequest.cs @@ -1,11 +1,18 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Session { + /// + /// A request to change the playstate of a session. + /// public class PlaystateRequest { + /// + /// Gets or sets the playstate command. + /// public PlaystateCommand Command { get; set; } + /// + /// Gets or sets the seek position in ticks. + /// public long? SeekPositionTicks { get; set; } /// diff --git a/MediaBrowser.Model/Session/QueueItem.cs b/MediaBrowser.Model/Session/QueueItem.cs index 43920a8464..b9f3181da0 100644 --- a/MediaBrowser.Model/Session/QueueItem.cs +++ b/MediaBrowser.Model/Session/QueueItem.cs @@ -1,13 +1,21 @@ #nullable disable -#pragma warning disable CS1591 using System; namespace MediaBrowser.Model.Session; +/// +/// An item in a play queue. +/// public record QueueItem { + /// + /// Gets or sets the item id. + /// public Guid Id { get; set; } + /// + /// Gets or sets the playlist item id. + /// public string PlaylistItemId { get; set; } } diff --git a/MediaBrowser.Model/Session/RepeatMode.cs b/MediaBrowser.Model/Session/RepeatMode.cs index c6e173d6b8..c6c657d220 100644 --- a/MediaBrowser.Model/Session/RepeatMode.cs +++ b/MediaBrowser.Model/Session/RepeatMode.cs @@ -1,11 +1,23 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Session { + /// + /// The repeat mode of a play queue. + /// public enum RepeatMode { + /// + /// Nothing is repeated. + /// RepeatNone = 0, + + /// + /// The whole queue is repeated. + /// RepeatAll = 1, + + /// + /// The current item is repeated. + /// RepeatOne = 2 } } -- cgit v1.2.3 From 8ac2d1f7bccfb265378888ec265304922154c90b Mon Sep 17 00:00:00 2001 From: mbastian77 Date: Sat, 18 Jul 2026 10:08:55 +0200 Subject: Add XML docs to small model enums and remove CS1591 suppressions --- .../Configuration/ImageSavingConvention.cs | 12 ++++++++++-- MediaBrowser.Model/Dto/IHasServerId.cs | 7 ++++++- MediaBrowser.Model/Dto/MediaSourceType.cs | 16 ++++++++++++++-- MediaBrowser.Model/Dto/RatingType.cs | 12 ++++++++++-- MediaBrowser.Model/Library/PlayAccess.cs | 12 ++++++++++-- MediaBrowser.Model/LiveTv/DayPattern.cs | 16 ++++++++++++++-- MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs | 12 ++++++++++-- MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs | 16 ++++++++++++++-- 8 files changed, 88 insertions(+), 15 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Model/Configuration/ImageSavingConvention.cs b/MediaBrowser.Model/Configuration/ImageSavingConvention.cs index 485a4d2f80..c67f379fde 100644 --- a/MediaBrowser.Model/Configuration/ImageSavingConvention.cs +++ b/MediaBrowser.Model/Configuration/ImageSavingConvention.cs @@ -1,10 +1,18 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Configuration { + /// + /// The convention used for naming saved images. + /// public enum ImageSavingConvention { + /// + /// The legacy naming convention. + /// Legacy, + + /// + /// The naming convention compatible with other media servers and metadata managers. + /// Compatible } } diff --git a/MediaBrowser.Model/Dto/IHasServerId.cs b/MediaBrowser.Model/Dto/IHasServerId.cs index c754d276c5..49452d736a 100644 --- a/MediaBrowser.Model/Dto/IHasServerId.cs +++ b/MediaBrowser.Model/Dto/IHasServerId.cs @@ -1,10 +1,15 @@ #nullable disable -#pragma warning disable CS1591 namespace MediaBrowser.Model.Dto { + /// + /// Interface for DTOs that reference the id of the server they originate from. + /// public interface IHasServerId { + /// + /// Gets the server id. + /// string ServerId { get; } } } diff --git a/MediaBrowser.Model/Dto/MediaSourceType.cs b/MediaBrowser.Model/Dto/MediaSourceType.cs index 42314d5198..ca6649e64b 100644 --- a/MediaBrowser.Model/Dto/MediaSourceType.cs +++ b/MediaBrowser.Model/Dto/MediaSourceType.cs @@ -1,11 +1,23 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Dto { + /// + /// The type of a media source. + /// public enum MediaSourceType { + /// + /// A default media source. + /// Default = 0, + + /// + /// A grouping of media sources. + /// Grouping = 1, + + /// + /// A placeholder media source, for example a disc that has to be inserted. + /// Placeholder = 2 } } diff --git a/MediaBrowser.Model/Dto/RatingType.cs b/MediaBrowser.Model/Dto/RatingType.cs index 033776f9c6..2c2b7b705d 100644 --- a/MediaBrowser.Model/Dto/RatingType.cs +++ b/MediaBrowser.Model/Dto/RatingType.cs @@ -1,10 +1,18 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Dto { + /// + /// The type of a community rating. + /// public enum RatingType { + /// + /// The rating is a numeric score. + /// Score, + + /// + /// The rating is based on likes. + /// Likes } } diff --git a/MediaBrowser.Model/Library/PlayAccess.cs b/MediaBrowser.Model/Library/PlayAccess.cs index a2f263ce54..22daaf7254 100644 --- a/MediaBrowser.Model/Library/PlayAccess.cs +++ b/MediaBrowser.Model/Library/PlayAccess.cs @@ -1,10 +1,18 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Library { + /// + /// The play access of an item. + /// public enum PlayAccess { + /// + /// The item can be played. + /// Full = 0, + + /// + /// The item cannot be played. + /// None = 1 } } diff --git a/MediaBrowser.Model/LiveTv/DayPattern.cs b/MediaBrowser.Model/LiveTv/DayPattern.cs index 17efe39088..dab69e8974 100644 --- a/MediaBrowser.Model/LiveTv/DayPattern.cs +++ b/MediaBrowser.Model/LiveTv/DayPattern.cs @@ -1,11 +1,23 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.LiveTv { + /// + /// The day pattern of a recurring timer. + /// public enum DayPattern { + /// + /// Every day. + /// Daily, + + /// + /// Monday through Friday. + /// Weekdays, + + /// + /// Saturday and Sunday. + /// Weekends } } diff --git a/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs b/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs index 72a0e2d7bf..a3df1dc411 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs @@ -1,10 +1,18 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.LiveTv { + /// + /// The status of a live TV service. + /// public enum LiveTvServiceStatus { + /// + /// The service is available. + /// Ok = 0, + + /// + /// The service is unavailable. + /// Unavailable = 1 } } diff --git a/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs b/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs index b7ee5747ab..1988dd8078 100644 --- a/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs +++ b/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs @@ -1,11 +1,23 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.MediaInfo { + /// + /// The type of timestamps used in a transport stream. + /// public enum TransportStreamTimestamp { + /// + /// The stream contains no timestamps. + /// None, + + /// + /// The stream contains zero-value timestamps. + /// Zero, + + /// + /// The stream contains valid timestamps. + /// Valid } } -- cgit v1.2.3 From 2cd2f36fe4912cb465cf5e78a055463f8a5c44a9 Mon Sep 17 00:00:00 2001 From: 854562 <44002186+854562@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:00:10 +0200 Subject: Extract truncation logic to helper and add tests --- .../Localization/LocalizationManager.cs | 18 ++++++++++++ .../Item/MediaStreamRepository.cs | 6 +--- .../Probing/ProbeResultNormalizer.cs | 8 ++--- .../Globalization/ILocalizationManager.cs | 8 +++++ .../Localization/LocalizationManagerTests.cs | 34 ++++++++++++++++++++++ 5 files changed, 63 insertions(+), 11 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 843e35afcc..068dde639e 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -261,6 +261,24 @@ namespace Emby.Server.Implementations.Localization _cultures); } + /// + public string? GetLanguageDisplayName(string language) + { + if (string.IsNullOrEmpty(language)) + { + return null; + } + + var displayName = FindLanguageInfo(language)?.DisplayName; + if (displayName is null) + { + return null; + } + + // Truncate at the first delimiter to avoid cluttered display names + return displayName.Split([';', ','], StringSplitOptions.None)[0].Trim(); + } + /// public IReadOnlyList GetCountries() { diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index 17cfdffe84..a25629132b 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -172,11 +172,7 @@ public class MediaStreamRepository : IMediaStreamRepository if (!string.IsNullOrEmpty(dto.Language)) { - var culture = _localization.FindLanguageInfo(dto.Language); - // Truncate at the first delimiter to avoid cluttered display names - dto.LocalizedLanguage = culture?.DisplayName is { } name - ? name.Split([';', ','])[0].Trim() - : null; + dto.LocalizedLanguage = _localization.GetLanguageDisplayName(dto.Language); } if (dto.Type is MediaStreamType.Audio) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index a9c37c0025..449c18a306 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -732,9 +732,7 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedOriginal = _localization.GetLocalizedString("Original"); if (!string.IsNullOrEmpty(stream.Language)) { - stream.LocalizedLanguage = _localization.FindLanguageInfo(stream.Language)?.DisplayName is { } name - ? name.Split([';', ','])[0].Trim() - : null; + stream.LocalizedLanguage = _localization.GetLanguageDisplayName(stream.Language); } stream.Channels = streamInfo.Channels; @@ -776,9 +774,7 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); if (!string.IsNullOrEmpty(stream.Language)) { - stream.LocalizedLanguage = _localization.FindLanguageInfo(stream.Language)?.DisplayName is { } name - ? name.Split([';', ','])[0].Trim() - : null; + stream.LocalizedLanguage = _localization.GetLanguageDisplayName(stream.Language); } if (string.IsNullOrEmpty(stream.Title)) diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index 7ad240abfb..0fff70c4e0 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs @@ -72,6 +72,14 @@ public interface ILocalizationManager /// The correct for the given language. CultureDto? FindLanguageInfo(string language); + /// + /// Gets a human-readable display name for the given language code. + /// Truncates at the first semicolon or comma to avoid cluttered ISO-639-2 names. + /// + /// An ISO language code. + /// The display name, or null if not found. + string? GetLanguageDisplayName(string language); + /// /// Returns the language in ISO 639-2/T when the input is ISO 639-2/B. /// diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 3b8fe5ca60..b608d3ea49 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -119,6 +119,40 @@ namespace Jellyfin.Server.Implementations.Tests.Localization Assert.Equal(code, culture.ThreeLetterISOLanguageName); } + [Theory] + [InlineData("ell", "Greek")] // Comma truncation + [InlineData("nld", "Dutch")] // Semicolon truncation + [InlineData("ron", "Romanian")] // Semicolon truncation, multiple + [InlineData("eng", "English")] // No truncation + [InlineData("zh-CN", "Chinese (Simplified)")] // No truncation, with parentheses + public async Task GetLanguageDisplayName_DelimitedName_ReturnsTruncatedName(string language, string expected) + { + var localizationManager = Setup(new ServerConfiguration + { + UICulture = "en-US" + }); + await localizationManager.LoadAll(); + + var result = localizationManager.GetLanguageDisplayName(language); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("xyz")] + public async Task GetLanguageDisplayName_InvalidInput_ReturnsNull(string? language) + { + var localizationManager = Setup(new ServerConfiguration + { + UICulture = "en-US" + }); + await localizationManager.LoadAll(); + + var result = localizationManager.GetLanguageDisplayName(language!); + Assert.Null(result); + } + [Fact] public async Task GetParentalRatings_Default_Success() { -- cgit v1.2.3