From 159a244654aa533a149fe00636c9c8e8fa5d2c01 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants Date: Sat, 19 Nov 2022 14:14:41 -0600 Subject: [PATCH 01/97] Add Options to disable DVR NFO and image saving - SaveRecordingNFO and SaveRecordingImages default to true. Maintains current behavior. - Episode.FillMissingEpisodeNumbersFromPath for live tv so external metadata can be pulled when recording starts. --- .../LiveTv/EmbyTV/EmbyTV.cs | 30 +++++++++++-------- .../Entities/TV/Episode.cs | 2 +- MediaBrowser.Model/LiveTv/LiveTvOptions.cs | 4 +++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index a0ae328a47..6739c10c1e 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1814,21 +1814,27 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV program.AddGenre("News"); } - if (timer.IsProgramSeries) + if (GetConfiguration().SaveRecordingNFO) { - await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false); - await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); - } - else if (!timer.IsMovie || timer.IsSports || timer.IsNews) - { - await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false); - } - else - { - await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); + if (timer.IsProgramSeries) + { + await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false); + await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); + } + else if (!timer.IsMovie || timer.IsSports || timer.IsNews) + { + await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false); + } + else + { + await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); + } } - await SaveRecordingImages(recordingPath, program).ConfigureAwait(false); + if (GetConfiguration().SaveRecordingImages) + { + await SaveRecordingImages(recordingPath, program).ConfigureAwait(false); + } } catch (Exception ex) { diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 15b721fe63..285d3ef085 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -320,7 +320,7 @@ namespace MediaBrowser.Controller.Entities.TV if (!IsLocked) { - if (SourceType == SourceType.Library) + if (SourceType == SourceType.Library || SourceType == SourceType.LiveTV) { try { diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs index 4cece941cf..25e5c77969 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs @@ -40,5 +40,9 @@ namespace MediaBrowser.Model.LiveTv public string RecordingPostProcessor { get; set; } public string RecordingPostProcessorArguments { get; set; } + + public bool SaveRecordingNFO { get; set; } = true; + + public bool SaveRecordingImages { get; set; } = true; } } From 8f4ac1cb811ac4d62512149a5feb01703004b058 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants Date: Sun, 27 Nov 2022 13:13:11 -0600 Subject: [PATCH 02/97] Call GetConfiguration just once in function --- Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 6739c10c1e..f27978a449 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1814,7 +1814,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV program.AddGenre("News"); } - if (GetConfiguration().SaveRecordingNFO) + var config = GetConfiguration(); + + if (config.SaveRecordingNFO) { if (timer.IsProgramSeries) { @@ -1831,7 +1833,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - if (GetConfiguration().SaveRecordingImages) + if (config.SaveRecordingImages) { await SaveRecordingImages(recordingPath, program).ConfigureAwait(false); } From 697efec86ecdca90d879ca65a18f4319f604990e Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 26 Mar 2022 12:11:00 +0100 Subject: [PATCH 03/97] Cleanup and refactor streambuilder --- Emby.Dlna/Didl/DidlBuilder.cs | 4 +- Emby.Dlna/PlayTo/PlayToController.cs | 4 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 6 +- .../Dlna/{AudioOptions.cs => MediaOptions.cs} | 24 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 374 +++++++++--------- MediaBrowser.Model/Dlna/VideoOptions.cs | 16 - .../Dlna/StreamBuilderTests.cs | 101 ++--- 7 files changed, 274 insertions(+), 255 deletions(-) rename MediaBrowser.Model/Dlna/{AudioOptions.cs => MediaOptions.cs} (79%) delete mode 100644 MediaBrowser.Model/Dlna/VideoOptions.cs diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index e9041186f4..bea7a5a0da 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -195,7 +195,7 @@ namespace Emby.Dlna.Didl { var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user); - streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions + streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions { ItemId = video.Id, MediaSources = sources.ToArray(), @@ -537,7 +537,7 @@ namespace Emby.Dlna.Didl { var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user); - streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions + streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions { ItemId = audio.Id, MediaSources = sources.ToArray(), diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 4cda1d8b7a..7b1f942c5a 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -585,7 +585,7 @@ namespace Emby.Dlna.PlayTo { return new PlaylistItem { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions + StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions { ItemId = item.Id, MediaSources = mediaSources, @@ -605,7 +605,7 @@ namespace Emby.Dlna.PlayTo { return new PlaylistItem { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions + StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions { ItemId = item.Id, MediaSources = mediaSources, diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index e8ce1ca2a2..e0245fe4da 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -181,7 +181,7 @@ namespace Jellyfin.Api.Helpers { var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); - var options = new VideoOptions + var options = new MediaOptions { MediaSources = new[] { mediaSource }, Context = EncodingContext.Streaming, @@ -244,8 +244,8 @@ namespace Jellyfin.Api.Helpers // Beginning of Playback Determination var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); + ? streamBuilder.GetOptimalAudioStream(options) + : streamBuilder.GetOptimalVideoStream(options); if (streamInfo is not null) { diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs similarity index 79% rename from MediaBrowser.Model/Dlna/AudioOptions.cs rename to MediaBrowser.Model/Dlna/MediaOptions.cs index df4018fdd5..939caf813e 100644 --- a/MediaBrowser.Model/Dlna/AudioOptions.cs +++ b/MediaBrowser.Model/Dlna/MediaOptions.cs @@ -7,11 +7,11 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.Dlna { /// - /// Class AudioOptions. + /// Class MediaOptions. /// - public class AudioOptions + public class MediaOptions { - public AudioOptions() + public MediaOptions() { Context = EncodingContext.Streaming; @@ -27,8 +27,16 @@ namespace MediaBrowser.Model.Dlna public bool ForceDirectStream { get; set; } + /// + /// Gets or sets an override for allowing stream copy. + /// public bool AllowAudioStreamCopy { get; set; } + /// + /// Gets or sets an override for allowing stream copy. + /// + public bool AllowVideoStreamCopy { get; set; } + public Guid ItemId { get; set; } public MediaSourceInfo[] MediaSources { get; set; } @@ -65,6 +73,16 @@ namespace MediaBrowser.Model.Dlna /// The audio transcoding bitrate. public int? AudioTranscodingBitrate { get; set; } + /// + /// Gets or sets an override for the audio stream index. + /// + public int? AudioStreamIndex { get; set; } + + /// + /// Gets or sets an override for the subtitle stream index. + /// + public int? SubtitleStreamIndex { get; set; } + /// /// Gets the maximum bitrate. /// diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index af35e98eef..00c3234dde 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1,5 +1,4 @@ #nullable disable -#pragma warning disable CS1591 using System; using System.Collections.Generic; @@ -13,6 +12,9 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Model.Dlna { + /// + /// Class StreamBuilder. + /// public class StreamBuilder { // Aliases @@ -24,35 +26,49 @@ namespace MediaBrowser.Model.Dlna private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; + /// + /// Initializes a new instance of the class. + /// + /// The object. + /// The object. public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger) { _transcoderSupport = transcoderSupport; _logger = logger; } + /// + /// Initializes a new instance of the class. + /// + /// The object. public StreamBuilder(ILogger logger) : this(new FullTranscoderSupport(), logger) { } - public StreamInfo BuildAudioItem(AudioOptions options) + /// + /// Gets the optimal audio stream. + /// + /// The object to get the audio stream from. + /// The of the optimal audio stream. + public StreamInfo GetOptimalAudioStream(MediaOptions options) { - ValidateAudioInput(options); + ValidateMediaOptions(options, false); var mediaSources = new List(); - foreach (MediaSourceInfo i in options.MediaSources) + foreach (var mediaSource in options.MediaSources) { if (string.IsNullOrEmpty(options.MediaSourceId) || - string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) + string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) { - mediaSources.Add(i); + mediaSources.Add(mediaSource); } } var streams = new List(); foreach (MediaSourceInfo i in mediaSources) { - StreamInfo streamInfo = BuildAudioItem(i, options); + StreamInfo streamInfo = GetOptimalAudioStream(i, options); if (streamInfo is not null) { streams.Add(streamInfo); @@ -68,9 +84,115 @@ namespace MediaBrowser.Model.Dlna return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0); } - public StreamInfo BuildVideoItem(VideoOptions options) + private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options) { - ValidateInput(options); + var playlistItem = new StreamInfo + { + ItemId = options.ItemId, + MediaType = DlnaProfileType.Audio, + MediaSource = item, + RunTimeTicks = item.RunTimeTicks, + Context = options.Context, + DeviceProfile = options.Profile + }; + + if (options.ForceDirectPlay) + { + playlistItem.PlayMethod = PlayMethod.DirectPlay; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); + return playlistItem; + } + + if (options.ForceDirectStream) + { + playlistItem.PlayMethod = PlayMethod.DirectStream; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); + return playlistItem; + } + + MediaStream audioStream = item.GetDefaultAudioStream(null); + + var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); + + var directPlayMethod = directPlayInfo.PlayMethod; + var transcodeReasons = directPlayInfo.TranscodeReasons; + + var inputAudioChannels = audioStream?.Channels; + var inputAudioBitrate = audioStream?.BitDepth; + var inputAudioSampleRate = audioStream?.SampleRate; + var inputAudioBitDepth = audioStream?.BitDepth; + + if (directPlayMethod.HasValue) + { + var profile = options.Profile; + var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true); + var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions); + transcodeReasons |= audioFailureReasons; + + if (audioFailureReasons == 0) + { + playlistItem.PlayMethod = directPlayMethod.Value; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile); + + return playlistItem; + } + } + + TranscodingProfile transcodingProfile = null; + foreach (TranscodingProfile i in options.Profile.TranscodingProfiles) + { + if (i.Type == playlistItem.MediaType + && i.Context == options.Context + && _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container)) + { + transcodingProfile = i; + break; + } + } + + if (transcodingProfile != null) + { + if (!item.SupportsTranscoding) + { + return null; + } + + SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); + + var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray(); + ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); + + // Honor requested max channels + playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; + + var configuredBitrate = options.GetMaxBitrate(true); + + long transcodingBitrate = options.AudioTranscodingBitrate ?? + (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ?? + configuredBitrate ?? + 128000; + + if (configuredBitrate.HasValue) + { + transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate); + } + + var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate); + playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); + } + + playlistItem.TranscodeReasons = transcodeReasons; + return playlistItem; + } + + /// + /// Gets the optimal video stream. + /// + /// The object to get the video stream from. + /// The of the optimal video stream. + public StreamInfo GetOptimalVideoStream(MediaOptions options) + { + ValidateMediaOptions(options, true); var mediaSources = new List(); foreach (MediaSourceInfo i in options.MediaSources) @@ -236,6 +358,14 @@ namespace MediaBrowser.Model.Dlna } } + /// + /// Normalizes input container. + /// + /// The input container. + /// The . + /// The . + /// The object to get the video stream from. + /// The the normalized input container. public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null) { if (string.IsNullOrEmpty(inputContainer)) @@ -264,108 +394,7 @@ namespace MediaBrowser.Model.Dlna return formats[0]; } - private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options) - { - StreamInfo playlistItem = new StreamInfo - { - ItemId = options.ItemId, - MediaType = DlnaProfileType.Audio, - MediaSource = item, - RunTimeTicks = item.RunTimeTicks, - Context = options.Context, - DeviceProfile = options.Profile - }; - - if (options.ForceDirectPlay) - { - playlistItem.PlayMethod = PlayMethod.DirectPlay; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); - return playlistItem; - } - - if (options.ForceDirectStream) - { - playlistItem.PlayMethod = PlayMethod.DirectStream; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); - return playlistItem; - } - - var audioStream = item.GetDefaultAudioStream(null); - - var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); - - var directPlayMethod = directPlayInfo.PlayMethod; - var transcodeReasons = directPlayInfo.TranscodeReasons; - - int? inputAudioChannels = audioStream?.Channels; - int? inputAudioBitrate = audioStream?.BitDepth; - int? inputAudioSampleRate = audioStream?.SampleRate; - int? inputAudioBitDepth = audioStream?.BitDepth; - - if (directPlayMethod.HasValue) - { - var profile = options.Profile; - var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true); - var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions); - transcodeReasons |= audioFailureReasons; - - if (audioFailureReasons == 0) - { - playlistItem.PlayMethod = directPlayMethod.Value; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile); - - return playlistItem; - } - } - - TranscodingProfile transcodingProfile = null; - foreach (var i in options.Profile.TranscodingProfiles) - { - if (i.Type == playlistItem.MediaType - && i.Context == options.Context - && _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container)) - { - transcodingProfile = i; - break; - } - } - - if (transcodingProfile is not null) - { - if (!item.SupportsTranscoding) - { - return null; - } - - SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); - - var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray(); - ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); - - // Honor requested max channels - playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - - var configuredBitrate = options.GetMaxBitrate(true); - - long transcodingBitrate = options.AudioTranscodingBitrate ?? - (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ?? - configuredBitrate ?? - 128000; - - if (configuredBitrate.HasValue) - { - transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate); - } - - var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate); - playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); - } - - playlistItem.TranscodeReasons = transcodeReasons; - return playlistItem; - } - - private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) + private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options) { var directPlayProfile = options.Profile.DirectPlayProfiles .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)); @@ -388,7 +417,7 @@ namespace MediaBrowser.Model.Dlna // If device requirements are satisfied then allow both direct stream and direct play if (item.SupportsDirectPlay) { - if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay)) + if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0)) { if (options.EnableDirectPlay) { @@ -404,7 +433,7 @@ namespace MediaBrowser.Model.Dlna // While options takes the network and other factors into account. Only applies to direct stream if (item.SupportsDirectStream) { - if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) + if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0)) { if (options.EnableDirectStream) { @@ -427,7 +456,6 @@ namespace MediaBrowser.Model.Dlna var containerSupported = false; var audioSupported = false; var videoSupported = false; - TranscodeReason reasons = 0; foreach (var profile in directPlayProfiles) { @@ -447,6 +475,7 @@ namespace MediaBrowser.Model.Dlna } } + TranscodeReason reasons = 0; if (!containerSupported) { reasons |= TranscodeReason.ContainerNotSupported; @@ -547,7 +576,7 @@ namespace MediaBrowser.Model.Dlna } } - private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) + private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) { var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); var protocol = "http"; @@ -562,7 +591,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); } - private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options) + private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options) { ArgumentNullException.ThrowIfNull(item); @@ -601,11 +630,15 @@ namespace MediaBrowser.Model.Dlna var videoStream = item.VideoStream; - var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay); - var directStreamBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream); - bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayBitrateEligibility == 0); - bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamBitrateEligibility == 0); - var transcodeReasons = directPlayBitrateEligibility | directStreamBitrateEligibility; + var bitrateLimitExceeded = IsBitrateLimitExceeded(item, options.GetMaxBitrate(false) ?? 0); + var isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || !bitrateLimitExceeded); + var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded); + TranscodeReason transcodeReasons = 0; + + if (bitrateLimitExceeded) + { + transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit; + } _logger.LogDebug( "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", @@ -702,7 +735,7 @@ namespace MediaBrowser.Model.Dlna } } - _logger.LogInformation( + _logger.LogDebug( "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}", options.Profile.Name ?? "Anonymous Profile", item.Path ?? "Unknown path", @@ -716,7 +749,7 @@ namespace MediaBrowser.Model.Dlna return playlistItem; } - private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) + private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) { if (!(item.SupportsTranscoding || item.SupportsDirectStream)) { @@ -763,7 +796,7 @@ namespace MediaBrowser.Model.Dlna return transcodingProfiles.FirstOrDefault(); } - private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, string container, string videoCodec, string audioCodec) + private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, string container, string videoCodec, string audioCodec) { // Prefer matching video codecs var videoCodecs = ContainerProfile.SplitValue(videoCodec); @@ -867,7 +900,7 @@ namespace MediaBrowser.Model.Dlna // Honor requested max channels playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); + int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream); @@ -882,14 +915,14 @@ namespace MediaBrowser.Model.Dlna i.ContainsAnyCodec(audioCodec, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); isFirstAppliedCodecProfile = true; - foreach (var i in appliedAudioConditions) + foreach (var codecProfile in appliedAudioConditions) { var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); foreach (var transcodingAudioCodec in transcodingAudioCodecs) { - if (i.ContainsAnyCodec(transcodingAudioCodec, container)) + if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container)) { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); + ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); isFirstAppliedCodecProfile = false; break; } @@ -1050,7 +1083,7 @@ namespace MediaBrowser.Model.Dlna } private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( - VideoOptions options, + MediaOptions options, MediaSourceInfo mediaSource, MediaStream videoStream, MediaStream audioStream, @@ -1237,7 +1270,7 @@ namespace MediaBrowser.Model.Dlna return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); } - private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) + private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) { var profile = options.Profile; var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream)); @@ -1274,23 +1307,17 @@ namespace MediaBrowser.Model.Dlna mediaSource.Path ?? "Unknown path"); } - private TranscodeReason IsBitrateEligibleForDirectPlayback( - MediaSourceInfo item, - long maxBitrate, - VideoOptions options, - PlayMethod playMethod) - { - bool result = IsItemBitrateEligibleForDirectPlayback(item, maxBitrate, playMethod); - if (!result) - { - return TranscodeReason.ContainerBitrateExceedsLimit; - } - else - { - return 0; - } - } - + /// + /// Normalizes input container. + /// + /// The . + /// The of the subtitle stream. + /// The list of supported s. + /// The . + /// The . + /// The output container. + /// The subtitle transoding protocol. + /// The the normalized input container. public static SubtitleProfile GetSubtitleProfile( MediaSourceInfo mediaSource, MediaStream subtitleStream, @@ -1448,14 +1475,8 @@ namespace MediaBrowser.Model.Dlna return null; } - private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) + private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate) { - // Don't restrict by bitrate if coming from an external domain - if (item.IsRemote) - { - return true; - } - long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000; // If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps @@ -1464,40 +1485,22 @@ namespace MediaBrowser.Model.Dlna if (itemBitrate > requestedMaxBitrate) { _logger.LogDebug( - "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", - playMethod, + "Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", itemBitrate, requestedMaxBitrate); - return false; + return true; } - return true; + return false; } - private static void ValidateInput(VideoOptions options) - { - ValidateAudioInput(options); - - if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) - { - throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested"); - } - - if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) - { - throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"); - } - } - - private static void ValidateAudioInput(AudioOptions options) + private static void ValidateMediaOptions(MediaOptions options, Boolean IsMediaSource) { if (options.ItemId.Equals(default)) { - throw new ArgumentException("ItemId is required"); + ArgumentException.ThrowIfNullOrEmpty(options.DeviceId); } - ArgumentException.ThrowIfNullOrEmpty(options.DeviceId); - if (options.Profile is null) { throw new ArgumentException("Profile is required"); @@ -1507,6 +1510,19 @@ namespace MediaBrowser.Model.Dlna { throw new ArgumentException("MediaSources is required"); } + + if (IsMediaSource) + { + if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) + { + throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested"); + } + + if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) + { + throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"); + } + } } private static IEnumerable GetProfileConditionsForVideoAudio( @@ -1824,8 +1840,8 @@ namespace MediaBrowser.Model.Dlna continue; } - // change from split by | to comma - // strip spaces to avoid having to encode + // Change from split by | to comma + // Strip spaces to avoid having to encode var values = value .Split('|', StringSplitOptions.RemoveEmptyEntries); diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs deleted file mode 100644 index 0cb80af544..0000000000 --- a/MediaBrowser.Model/Dlna/VideoOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Dlna -{ - /// - /// Class VideoOptions. - /// - public class VideoOptions : AudioOptions - { - public int? AudioStreamIndex { get; set; } - - public int? SubtitleStreamIndex { get; set; } - - public bool AllowVideoStreamCopy { get; set; } - } -} diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 5e11a7232d..60be17a741 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -164,7 +164,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); } @@ -262,7 +262,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); options.AudioStreamIndex = 1; options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1; @@ -298,7 +298,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); var streamCount = options.MediaSources[0].MediaStreams.Count; if (streamCount > 0) { @@ -311,7 +311,7 @@ namespace Jellyfin.Model.Tests Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); } - private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) + private StreamInfo? BuildVideoItemSimpleTest(MediaOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) { if (string.IsNullOrEmpty(transcodeProtocol)) { @@ -320,28 +320,28 @@ namespace Jellyfin.Model.Tests var builder = GetStreamBuilder(); - var val = builder.BuildVideoItem(options); - Assert.NotNull(val); + var streamInfo = builder.GetOptimalVideoStream(options); + Assert.NotNull(streamInfo); if (playMethod is not null) { - Assert.Equal(playMethod, val.PlayMethod); + Assert.Equal(playMethod, streamInfo.PlayMethod); } - Assert.Equal(why, val.TranscodeReasons); + Assert.Equal(why, streamInfo.TranscodeReasons); var audioStreamIndexInput = options.AudioStreamIndex; - var targetVideoStream = val.TargetVideoStream; - var targetAudioStream = val.TargetAudioStream; + var targetVideoStream = streamInfo.TargetVideoStream; + var targetAudioStream = streamInfo.TargetAudioStream; - var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId); + var mediaSource = options.MediaSources.First(source => source.Id == streamInfo.MediaSourceId); Assert.NotNull(mediaSource); var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video); var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio); // TODO: Check AudioStreamIndex vs options.AudioStreamIndex var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex); - var uri = ParseUri(val); + var uri = ParseUri(streamInfo); if (playMethod == PlayMethod.DirectPlay) { @@ -351,98 +351,99 @@ namespace Jellyfin.Model.Tests // Assert.Contains(uri.Extension, containers); // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); - Assert.Single(val.TargetVideoCodec); + Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec); - Assert.Single(val.TargetAudioCodec); + Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec); + Assert.Single(streamInfo.TargetAudioCodec); // Assert.Single(val.AudioCodecs); if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { - Assert.Equal(val.Container, uri.Extension); + Assert.Equal(streamInfo.Container, uri.Extension); } } else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) { - Assert.NotNull(val.Container); - Assert.NotEmpty(val.VideoCodecs); - Assert.NotEmpty(val.AudioCodecs); + Assert.NotNull(streamInfo.Container); + Assert.NotEmpty(streamInfo.VideoCodecs); + Assert.NotEmpty(streamInfo.AudioCodecs); // Check expected container (todo: this could be a test param) if (transcodeProtocol.Equals("http", StringComparison.Ordinal)) { // Assert.Equal("webm", val.Container); - Assert.Equal(val.Container, uri.Extension); + Assert.Equal(streamInfo.Container, uri.Extension); Assert.Equal("stream", uri.Filename); - Assert.Equal("http", val.SubProtocol); + Assert.Equal("http", streamInfo.SubProtocol); } else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal)) { - Assert.Equal("mp4", val.Container); + Assert.Equal("mp4", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", val.SubProtocol); + Assert.Equal("hls", streamInfo.SubProtocol); } else { - Assert.Equal("ts", val.Container); + Assert.Equal("ts", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", val.SubProtocol); + Assert.Equal("hls", streamInfo.SubProtocol); } // Full transcode if (transcodeMode.Equals("Transcode", StringComparison.Ordinal)) { - if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) + if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) { Assert.All( videoStreams, - stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); + stream => Assert.DoesNotContain(stream.Codec, streamInfo.VideoCodecs)); } - // TODO: Fill out tests here + // TODO: fill out tests here } // DirectStream and Remux else { // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); - Assert.Single(val.TargetVideoCodec); + Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { // Check expected audio codecs (1) if (!targetAudioStream.IsExternal) { - if (val.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) + // Check expected audio codecs (1) + if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) { - Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); + Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); } else { - Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } } } else if (transcodeMode.Equals("Remux", StringComparison.Ordinal)) { // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); - Assert.Single(val.AudioCodecs); + Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); + Assert.Single(streamInfo.AudioCodecs); } // Video details var videoStream = targetVideoStream; - Assert.False(val.EstimateContentLength); - Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); - Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty()); - Assert.Equal(videoStream.Level, val.TargetVideoLevel); - Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth); - Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); + Assert.False(streamInfo.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); + Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty()); + Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel); + Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth); + Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); // Audio codec not supported if ((why & TranscodeReason.AudioCodecNotSupported) != 0) @@ -453,7 +454,7 @@ namespace Jellyfin.Model.Tests // TODO:fixme if (!targetAudioStream.IsExternal) { - Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } } @@ -465,7 +466,7 @@ namespace Jellyfin.Model.Tests { if (!stream.IsExternal) { - Assert.DoesNotContain(stream.Codec, val.AudioCodecs); + Assert.DoesNotContain(stream.Codec, streamInfo.AudioCodecs); } }); } @@ -474,14 +475,14 @@ namespace Jellyfin.Model.Tests } else if (playMethod is null) { - Assert.Null(val.SubProtocol); + Assert.Null(streamInfo.SubProtocol); Assert.Equal("stream", uri.Filename); - Assert.False(val.EstimateContentLength); - Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); + Assert.False(streamInfo.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); } - return val; + return streamInfo; } private static async ValueTask TestData(string name) @@ -507,7 +508,7 @@ namespace Jellyfin.Model.Tests return new StreamBuilder(transcodeSupport.Object, logger); } - private static async ValueTask GetVideoOptions(string deviceProfile, params string[] sources) + private static async ValueTask GetMediaOptions(string deviceProfile, params string[] sources) { var mediaSources = sources.Select(src => TestData(src)) .Select(val => val.Result) @@ -516,7 +517,7 @@ namespace Jellyfin.Model.Tests var dp = await TestData(deviceProfile); - return new VideoOptions() + return new MediaOptions() { ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"), MediaSourceId = mediaSourceId, From 1cd7da8889e5b243d738ac8445f36526f1cae42b Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 4 Dec 2022 12:09:10 +0100 Subject: [PATCH 04/97] Apply review suggestions --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 00c3234dde..1cf25a99f4 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1477,6 +1477,13 @@ namespace MediaBrowser.Model.Dlna private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate) { + // Don't restrict by bitrate if coming from an external domain + if (item.IsRemote) + { + return false; + } + + long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000; // If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps From 08a5c71b908f18fa1feb020ed2a1cc227bdc5491 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 4 Dec 2022 12:31:05 +0100 Subject: [PATCH 05/97] Add xmldoc for MediaOptions --- MediaBrowser.Model/Dlna/MediaOptions.cs | 35 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Model/Dlna/MediaOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs index 939caf813e..71ee1db05d 100644 --- a/MediaBrowser.Model/Dlna/MediaOptions.cs +++ b/MediaBrowser.Model/Dlna/MediaOptions.cs @@ -1,5 +1,4 @@ #nullable disable -#pragma warning disable CS1591 using System; using MediaBrowser.Model.Dto; @@ -11,6 +10,9 @@ namespace MediaBrowser.Model.Dlna /// public class MediaOptions { + /// + /// Creates a new instance of the class. + /// public MediaOptions() { Context = EncodingContext.Streaming; @@ -19,28 +21,50 @@ namespace MediaBrowser.Model.Dlna EnableDirectStream = true; } + /// + /// Gets or sets a boolean to allow/forbid direct playback. + /// public bool EnableDirectPlay { get; set; } + /// + /// Gets or sets a boolean to allow/forbid direct streaming. + /// public bool EnableDirectStream { get; set; } + /// + /// Gets or sets a boolean to force direct playback. + /// public bool ForceDirectPlay { get; set; } + /// + /// Gets or sets a boolean to force direct streaming. + /// public bool ForceDirectStream { get; set; } /// - /// Gets or sets an override for allowing stream copy. + /// Gets or sets a boolean to allow/forbid audio stream copy. /// public bool AllowAudioStreamCopy { get; set; } /// - /// Gets or sets an override for allowing stream copy. + /// Gets or sets a boolean to allow/forbid video stream copy. /// public bool AllowVideoStreamCopy { get; set; } + /// + /// Gets or sets the item id. + /// public Guid ItemId { get; set; } + /// + /// Gets or sets the media sources. + /// public MediaSourceInfo[] MediaSources { get; set; } + /// + /// Gets or sets the device profile. + /// + public DeviceProfile Profile { get; set; } /// @@ -48,6 +72,9 @@ namespace MediaBrowser.Model.Dlna /// public string MediaSourceId { get; set; } + /// + /// Gets or sets the device id. + /// public string DeviceId { get; set; } /// @@ -57,7 +84,7 @@ namespace MediaBrowser.Model.Dlna public int? MaxAudioChannels { get; set; } /// - /// Gets or sets the application's configured quality setting. + /// Gets or sets the application's configured maximum bitrate. /// public int? MaxBitrate { get; set; } From 8e8a085b7ec9649a0b5968aa6c926b8aff576b39 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 4 Dec 2022 12:41:46 +0100 Subject: [PATCH 06/97] Prefer var in StreamBuilder --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 36 ++++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 1cf25a99f4..bb1e236bea 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -66,16 +66,16 @@ namespace MediaBrowser.Model.Dlna } var streams = new List(); - foreach (MediaSourceInfo i in mediaSources) + foreach (var mediaSourceInfo in mediaSources) { - StreamInfo streamInfo = GetOptimalAudioStream(i, options); + StreamInfo streamInfo = GetOptimalAudioStream(mediaSourceInfo, options); if (streamInfo is not null) { streams.Add(streamInfo); } } - foreach (StreamInfo stream in streams) + foreach (var stream in streams) { stream.DeviceId = options.DeviceId; stream.DeviceProfileId = options.Profile.Id; @@ -139,13 +139,13 @@ namespace MediaBrowser.Model.Dlna } TranscodingProfile transcodingProfile = null; - foreach (TranscodingProfile i in options.Profile.TranscodingProfiles) + foreach (var tcProfile in options.Profile.TranscodingProfiles) { - if (i.Type == playlistItem.MediaType - && i.Context == options.Context - && _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container)) + if (tcProfile.Type == playlistItem.MediaType + && tcProfile.Context == options.Context + && _transcoderSupport.CanEncodeToAudioCodec(transcodingProfile.AudioCodec ?? tcProfile.Container)) { - transcodingProfile = i; + transcodingProfile = tcProfile; break; } } @@ -167,10 +167,10 @@ namespace MediaBrowser.Model.Dlna var configuredBitrate = options.GetMaxBitrate(true); - long transcodingBitrate = options.AudioTranscodingBitrate ?? - (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ?? - configuredBitrate ?? - 128000; + long transcodingBitrate = options.AudioTranscodingBitrate + ?? (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) + ?? configuredBitrate + ?? 128000; if (configuredBitrate.HasValue) { @@ -195,26 +195,26 @@ namespace MediaBrowser.Model.Dlna ValidateMediaOptions(options, true); var mediaSources = new List(); - foreach (MediaSourceInfo i in options.MediaSources) + foreach (var mediaSourceInfo in options.MediaSources) { if (string.IsNullOrEmpty(options.MediaSourceId) || - string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) + string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) { - mediaSources.Add(i); + mediaSources.Add(mediaSourceInfo); } } var streams = new List(); - foreach (MediaSourceInfo i in mediaSources) + foreach (var mediaSourceInfo in mediaSources) { - var streamInfo = BuildVideoItem(i, options); + var streamInfo = BuildVideoItem(mediaSourceInfo, options); if (streamInfo is not null) { streams.Add(streamInfo); } } - foreach (StreamInfo stream in streams) + foreach (var stream in streams) { stream.DeviceId = options.DeviceId; stream.DeviceProfileId = options.Profile.Id; From 0834dc58c13b42e599c78e3898229ecc09120754 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 7 Dec 2022 18:06:04 +0100 Subject: [PATCH 07/97] Fix .Net 7 compatibility --- MediaBrowser.Model/Dlna/MediaOptions.cs | 15 +++++++-------- MediaBrowser.Model/Dlna/StreamBuilder.cs | 5 ++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/MediaBrowser.Model/Dlna/MediaOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs index 71ee1db05d..29aecf97fc 100644 --- a/MediaBrowser.Model/Dlna/MediaOptions.cs +++ b/MediaBrowser.Model/Dlna/MediaOptions.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Dlna public class MediaOptions { /// - /// Creates a new instance of the class. + /// Initializes a new instance of the class. /// public MediaOptions() { @@ -22,32 +22,32 @@ namespace MediaBrowser.Model.Dlna } /// - /// Gets or sets a boolean to allow/forbid direct playback. + /// Gets or sets a value indicating whether direct playback is allowed. /// public bool EnableDirectPlay { get; set; } /// - /// Gets or sets a boolean to allow/forbid direct streaming. + /// Gets or sets a value indicating whether direct streaming is allowed. /// public bool EnableDirectStream { get; set; } /// - /// Gets or sets a boolean to force direct playback. + /// Gets or sets a value indicating whether direct playback is forced. /// public bool ForceDirectPlay { get; set; } /// - /// Gets or sets a boolean to force direct streaming. + /// Gets or sets a value indicating whether direct streaming is forced. /// public bool ForceDirectStream { get; set; } /// - /// Gets or sets a boolean to allow/forbid audio stream copy. + /// Gets or sets a value indicating whether audio stream copy is allowed. /// public bool AllowAudioStreamCopy { get; set; } /// - /// Gets or sets a boolean to allow/forbid video stream copy. + /// Gets or sets a value indicating whether video stream copy is allowed. /// public bool AllowVideoStreamCopy { get; set; } @@ -64,7 +64,6 @@ namespace MediaBrowser.Model.Dlna /// /// Gets or sets the device profile. /// - public DeviceProfile Profile { get; set; } /// diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index bb1e236bea..2f919b0451 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1483,7 +1483,6 @@ namespace MediaBrowser.Model.Dlna return false; } - long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000; // If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps @@ -1501,7 +1500,7 @@ namespace MediaBrowser.Model.Dlna return false; } - private static void ValidateMediaOptions(MediaOptions options, Boolean IsMediaSource) + private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource) { if (options.ItemId.Equals(default)) { @@ -1518,7 +1517,7 @@ namespace MediaBrowser.Model.Dlna throw new ArgumentException("MediaSources is required"); } - if (IsMediaSource) + if (isMediaSource) { if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) { From 9a35fd673203cfaf0098138b2768750f4818b3ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 14:32:34 +0100 Subject: [PATCH 08/97] chore(deps): update peter-evans/find-comment digest to 81e2da3 (#8930) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/openapi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 889133aedf..f426357a9a 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -95,7 +95,7 @@ jobs: body="${body//$'\r'/'%0D'}" echo ::set-output name=body::$body - name: Find difference comment - uses: peter-evans/find-comment@f4499a714d59013c74a08789b48abe4b704364a0 # v2 + uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} From 58006013f2837c3b2876644cfe3994ceaadb85ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Dec 2022 08:59:04 -0500 Subject: [PATCH 09/97] chore(deps): update actions/stale action to v7 (#8935) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/repo-stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index 1c6fe1492f..c1f5e718b8 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6 + - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 From 497b2765b9bc3d2a4aa4bc582dd8e6f456b47837 Mon Sep 17 00:00:00 2001 From: Mason McGlothlin Date: Fri, 23 Dec 2022 14:34:14 -0600 Subject: [PATCH 10/97] Correct path to installation guide (#8948) * Correct path to installation guide The installation guide link was broken. Updated it to the correct location. * Fix docs links to not include docs subdomain --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1963ba526d..2362741b47 100644 --- a/README.md +++ b/README.md @@ -47,16 +47,16 @@ Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest! -For further details, please see [our documentation page](https://docs.jellyfin.org/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://docs.jellyfin.org/general/getting-help.html). For more information about the project, please see our [about page](https://docs.jellyfin.org/general/about.html). +For further details, please see [our documentation page](https://jellyfin.org/docs/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://jellyfin.org/docs/general/getting-help). For more information about the project, please see our [about page](https://jellyfin.org/docs/general/about). Want to get started?
-Check out our downloads page or our installation guide, then see our quick start guide. You can also build from source.
+Check out our downloads page or our installation guide, then see our quick start guide. You can also build from source.
Something not working right?
-Open an Issue on GitHub.
+Open an Issue on GitHub.
Want to contribute?
-Check out our contributing choose-your-own-adventure to see where you can help, then see our contributing guide and our community standards.
+Check out our contributing choose-your-own-adventure to see where you can help, then see our contributing guide and our community standards.
New idea or improvement?
Check out our feature request hub.
From 2385588998b9b9c562c42b122fea6c421641ed3b Mon Sep 17 00:00:00 2001 From: gam24 Date: Thu, 22 Dec 2022 23:18:48 +0000 Subject: [PATCH 11/97] Translated using Weblate (Polish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pl/ --- Emby.Server.Implementations/Localization/Core/pl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index d0b458a8fb..d4c15ac876 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Kompaktuje bazę danych i obcina wolne miejsce. Uruchomienie tego zadania po przeskanowaniu biblioteki lub dokonaniu innych zmian, które pociągają za sobą modyfikacje bazy danych, może poprawić wydajność.", "External": "Zewnętrzny", "TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.", - "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych" + "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych", + "HearingImpaired": "Niedosłyszący" } From a6efdd850a551bd63cb4ca8459078352578d53e2 Mon Sep 17 00:00:00 2001 From: DuaLee Date: Sat, 24 Dec 2022 22:20:02 +0000 Subject: [PATCH 12/97] Translated using Weblate (Korean) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ko/ --- Emby.Server.Implementations/Localization/Core/ko.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index a4b2e75b30..67dcf5b049 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "데이터베이스 최적화", "TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.", "TaskKeyframeExtractor": "키프레임 추출", - "External": "외부" + "External": "외부", + "HearingImpaired": "청각 장애" } From ef4ea1eb77b805b9f57a6a14b18f155630354020 Mon Sep 17 00:00:00 2001 From: 0TTA Date: Mon, 26 Dec 2022 07:00:19 +0000 Subject: [PATCH 13/97] Translated using Weblate (Arabic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/ --- Emby.Server.Implementations/Localization/Core/ar.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index ada3c77301..4508363b0c 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -12,7 +12,7 @@ "DeviceOfflineWithName": "قُطِع الاتصال ب{0}", "DeviceOnlineWithName": "{0} متصل", "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}", - "Favorites": "مفضلات", + "Favorites": "المفضلة", "Folders": "المجلدات", "Genres": "التصنيفات", "HeaderAlbumArtists": "فناني الألبوم", @@ -91,13 +91,13 @@ "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}", "ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط", "ValueSpecialEpisodeName": "حلقه خاصه - {0}", - "VersionNumber": "النسخة {0}", + "VersionNumber": "الإصدار {0}", "TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.", "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة", "TasksChannelsCategory": "قنوات الإنترنت", "TasksLibraryCategory": "مكتبة", "TasksMaintenanceCategory": "صيانة", - "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.", + "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يُحدث البيانات الوصفية.", "TaskRefreshLibrary": "افحص مكتبة الوسائط", "TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.", "TaskRefreshChapterImages": "استخراج صور الفصل", @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "تحسين قاعدة البيانات", "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.", "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", - "External": "خارجي" + "External": "خارجي", + "HearingImpaired": "ضعاف السمع" } From f32d4040f23657346721ed234b9e29710adb3c9d Mon Sep 17 00:00:00 2001 From: NorwayFun Date: Mon, 26 Dec 2022 05:53:14 +0000 Subject: [PATCH 14/97] Translated using Weblate (Georgian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ka/ --- .../Localization/Core/ka.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index 3a8b89f446..dbbc81eeb2 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -108,5 +108,20 @@ "UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია", "UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა", "UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე", - "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა." + "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.", + "TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.", + "NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.", + "CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან", + "StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.", + "SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა", + "ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას", + "TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველი ჟურნალის ჩანაწერების წაშლა.", + "TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.", + "TaskRefreshLibraryDescription": "თქვენი მედია ბიბლიოთეკაში ახალი ფაილების ძებნა და მეტამონაცემების განახლება.", + "TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.", + "TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.", + "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.", + "TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.", + "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.", + "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს." } From f2200c97cbd79e5ff432948ee0fbd1e66ffa4e14 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 27 Dec 2022 14:24:34 +0100 Subject: [PATCH 15/97] Remove unused notification endpoints (#8952) --- .../Controllers/NotificationsController.cs | 87 +------------------ .../NotificationDtos/AdminNotificationDto.cs | 30 ------- .../NotificationDtos/NotificationDto.cs | 51 ----------- .../NotificationDtos/NotificationResultDto.cs | 21 ----- .../NotificationsSummaryDto.cs | 20 ----- 5 files changed, 1 insertion(+), 208 deletions(-) delete mode 100644 Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs delete mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs delete mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs delete mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 420630cdf4..a285564760 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,12 +1,5 @@ -using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading; using Jellyfin.Api.Constants; -using Jellyfin.Api.Models.NotificationDtos; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Notifications; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Notifications; @@ -23,41 +16,14 @@ namespace Jellyfin.Api.Controllers public class NotificationsController : BaseJellyfinApiController { private readonly INotificationManager _notificationManager; - private readonly IUserManager _userManager; /// /// Initializes a new instance of the class. /// /// The notification manager. - /// The user manager. - public NotificationsController(INotificationManager notificationManager, IUserManager userManager) + public NotificationsController(INotificationManager notificationManager) { _notificationManager = notificationManager; - _userManager = userManager; - } - - /// - /// Gets a user's notifications. - /// - /// Notifications returned. - /// An containing a list of notifications. - [HttpGet("{userId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetNotifications() - { - return new NotificationResultDto(); - } - - /// - /// Gets a user's notification summary. - /// - /// Summary of user's notifications returned. - /// An containing a summary of the users notifications. - [HttpGet("{userId}/Summary")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetNotificationsSummary() - { - return new NotificationsSummaryDto(); } /// @@ -83,56 +49,5 @@ namespace Jellyfin.Api.Controllers { return _notificationManager.GetNotificationServices(); } - - /// - /// Sends a notification to all admins. - /// - /// The notification request. - /// Notification sent. - /// A . - [HttpPost("Admin")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto) - { - var notification = new NotificationRequest - { - Name = notificationDto.Name, - Description = notificationDto.Description, - Url = notificationDto.Url, - Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal, - UserIds = _userManager.Users - .Where(user => user.HasPermission(PermissionKind.IsAdministrator)) - .Select(user => user.Id) - .ToArray(), - Date = DateTime.UtcNow, - }; - - _notificationManager.SendNotification(notification, CancellationToken.None); - return NoContent(); - } - - /// - /// Sets notifications as read. - /// - /// Notifications set as read. - /// A . - [HttpPost("{userId}/Read")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SetRead() - { - return NoContent(); - } - - /// - /// Sets notifications as unread. - /// - /// Notifications set as unread. - /// A . - [HttpPost("{userId}/Unread")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SetUnread() - { - return NoContent(); - } } } diff --git a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs deleted file mode 100644 index 2c3a6282f2..0000000000 --- a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MediaBrowser.Model.Notifications; - -namespace Jellyfin.Api.Models.NotificationDtos -{ - /// - /// The admin notification dto. - /// - public class AdminNotificationDto - { - /// - /// Gets or sets the notification name. - /// - public string? Name { get; set; } - - /// - /// Gets or sets the notification description. - /// - public string? Description { get; set; } - - /// - /// Gets or sets the notification level. - /// - public NotificationLevel? NotificationLevel { get; set; } - - /// - /// Gets or sets the notification url. - /// - public string? Url { get; set; } - } -} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs deleted file mode 100644 index af5239ec2b..0000000000 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using MediaBrowser.Model.Notifications; - -namespace Jellyfin.Api.Models.NotificationDtos -{ - /// - /// The notification DTO. - /// - public class NotificationDto - { - /// - /// Gets or sets the notification ID. Defaults to an empty string. - /// - public string Id { get; set; } = string.Empty; - - /// - /// Gets or sets the notification's user ID. Defaults to an empty string. - /// - public string UserId { get; set; } = string.Empty; - - /// - /// Gets or sets the notification date. - /// - public DateTime Date { get; set; } - - /// - /// Gets or sets a value indicating whether the notification has been read. Defaults to false. - /// - public bool IsRead { get; set; } = false; - - /// - /// Gets or sets the notification's name. Defaults to an empty string. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Gets or sets the notification's description. Defaults to an empty string. - /// - public string Description { get; set; } = string.Empty; - - /// - /// Gets or sets the notification's URL. Defaults to an empty string. - /// - public string Url { get; set; } = string.Empty; - - /// - /// Gets or sets the notification level. - /// - public NotificationLevel Level { get; set; } - } -} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs deleted file mode 100644 index 64e92bd83a..0000000000 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Jellyfin.Api.Models.NotificationDtos -{ - /// - /// A list of notifications with the total record count for pagination. - /// - public class NotificationResultDto - { - /// - /// Gets or sets the current page of notifications. - /// - public IReadOnlyList Notifications { get; set; } = Array.Empty(); - - /// - /// Gets or sets the total number of notifications. - /// - public int TotalRecordCount { get; set; } - } -} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs deleted file mode 100644 index 0568dea666..0000000000 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediaBrowser.Model.Notifications; - -namespace Jellyfin.Api.Models.NotificationDtos -{ - /// - /// The notification summary DTO. - /// - public class NotificationsSummaryDto - { - /// - /// Gets or sets the number of unread notifications. - /// - public int UnreadCount { get; set; } - - /// - /// Gets or sets the maximum unread notification level. - /// - public NotificationLevel? MaxUnreadNotificationLevel { get; set; } - } -} From 233879e525fb220b2eed09d7aaf33e26587906e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 14:29:14 +0100 Subject: [PATCH 16/97] chore(deps): update dependency libse to v3.6.10 (#8958) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 375041490b..7404c2868b 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -27,7 +27,7 @@ - + From 6bf131b2703f83a687faff948467492725e94136 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 27 Dec 2022 16:53:58 +0100 Subject: [PATCH 17/97] Use Diacritics.NET Last time we had to revert this due to regressions, now those regression tests seem to succeed with a newer version of Diacritics.NET --- .../Manager/MetadataService.cs | 4 ++-- .../Jellyfin.Extensions.csproj | 5 ++++ src/Jellyfin.Extensions/StringExtensions.cs | 24 ++++--------------- .../AlphanumericComparatorTests.cs | 2 +- .../StringExtensionsTests.cs | 2 ++ 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index ff06c7ce4d..96ef462397 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -444,8 +444,8 @@ namespace MediaBrowser.Providers.Manager } } - if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue) || - (originalProductionYear ?? -1) != (item.ProductionYear ?? -1)) + if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue) + || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1)) { updateType |= ItemUpdateType.MetadataEdit; } diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index eaf2bc35cc..9fed8cbd9a 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -27,6 +27,11 @@ + + + + + diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index b19be071bf..f30b639459 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -20,23 +20,8 @@ namespace Jellyfin.Extensions /// The string to act on. /// The string without diacritics character. public static string RemoveDiacritics(this string text) - { - string withDiactritics = _nonConformingUnicode - .Replace(text, string.Empty) - .Normalize(NormalizationForm.FormD); - - var withoutDiactritics = new StringBuilder(); - foreach (char c in withDiactritics) - { - UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(c); - if (uc != UnicodeCategory.NonSpacingMark) - { - withoutDiactritics.Append(c); - } - } - - return withoutDiactritics.ToString().Normalize(NormalizationForm.FormC); - } + => Diacritics.Extensions.StringExtensions.RemoveDiacritics( + _nonConformingUnicode.Replace(text, string.Empty)); /// /// Checks whether or not the specified string has diacritics in it. @@ -44,9 +29,8 @@ namespace Jellyfin.Extensions /// The string to check. /// True if the string has diacritics, false otherwise. public static bool HasDiacritics(this string text) - { - return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal); - } + => Diacritics.Extensions.StringExtensions.HasDiacritics(text) + || _nonConformingUnicode.IsMatch(text); /// /// Counts the number of occurrences of [needle] in the string. diff --git a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs index 7730841a1c..2a7e8fafdf 100644 --- a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs +++ b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Extensions.Tests { var copy = strings.Reverse().ToArray(); Array.Sort(copy, new AlphanumericComparator()); - Assert.True(strings.SequenceEqual(copy)); + Assert.Equal(strings, copy); } } } diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs index 903d88caa1..ef8cbef9dd 100644 --- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs +++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs @@ -9,6 +9,7 @@ namespace Jellyfin.Extensions.Tests [InlineData("", "")] // Identity edge-case (no diactritics) [InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diactritics) [InlineData("a\ud800b", "ab")] // Invalid UTF-16 char stripping + [InlineData("åäö", "aao")] // Issue #7484 [InlineData("Jön", "Jon")] // Issue #7484 [InlineData("Jönssonligan", "Jonssonligan")] // Issue #7484 [InlineData("Kieślowski", "Kieslowski")] // Issue #7450 @@ -25,6 +26,7 @@ namespace Jellyfin.Extensions.Tests [InlineData("", false)] // Identity edge-case (no diactritics) [InlineData("Indiana Jones", false)] // Identity (no diactritics) [InlineData("a\ud800b", true)] // Invalid UTF-16 char stripping + [InlineData("åäö", true)] // Issue #7484 [InlineData("Jön", true)] // Issue #7484 [InlineData("Jönssonligan", true)] // Issue #7484 [InlineData("Kieślowski", true)] // Issue #7450 From 7c77ba529c057b227b1597c408192334eb78aff4 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 27 Dec 2022 17:02:23 +0100 Subject: [PATCH 18/97] Add more tests --- tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs index ef8cbef9dd..69d20bd3fe 100644 --- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs +++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs @@ -16,6 +16,8 @@ namespace Jellyfin.Extensions.Tests [InlineData("Cidadão Kane", "Cidadao Kane")] // Issue #7560 [InlineData("운명처럼 널 사랑해", "운명처럼 널 사랑해")] // Issue #6393 (Korean language support) [InlineData("애타는 로맨스", "애타는 로맨스")] // Issue #6393 + [InlineData("Le cœur a ses raisons", "Le coeur a ses raisons")] // Issue #8893 + [InlineData("Béla Tarr", "Bela Tarr")] // Issue #8893 public void RemoveDiacritics_ValidInput_Corrects(string input, string expectedResult) { string result = input.RemoveDiacritics(); @@ -33,6 +35,8 @@ namespace Jellyfin.Extensions.Tests [InlineData("Cidadão Kane", true)] // Issue #7560 [InlineData("운명처럼 널 사랑해", false)] // Issue #6393 (Korean language support) [InlineData("애타는 로맨스", false)] // Issue #6393 + [InlineData("Le cœur a ses raisons", true)] // Issue #8893 + [InlineData("Béla Tarr", true)] // Issue #8893 public void HasDiacritics_ValidInput_Corrects(string input, bool expectedResult) { bool result = input.HasDiacritics(); From 8de052646ca0a852dcad03069a1904433f909653 Mon Sep 17 00:00:00 2001 From: guineu Date: Tue, 27 Dec 2022 14:49:16 +0000 Subject: [PATCH 19/97] Translated using Weblate (Catalan) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/ --- Emby.Server.Implementations/Localization/Core/ca.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index c0ed01fdff..50c574d674 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -118,7 +118,7 @@ "TaskCleanActivityLog": "Buidar Registre d'Activitat", "Undefined": "Indefinit", "Forced": "Forçat", - "Default": "Defecte", + "Default": "Per defecte", "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.", "TaskOptimizeDatabase": "Optimitzar la base de dades", "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", From 4ee1b78a800b1a525e30e5543cdeca9494a95a04 Mon Sep 17 00:00:00 2001 From: Brad Beattie Date: Thu, 29 Dec 2022 09:15:48 -0500 Subject: [PATCH 20/97] Fix image searching to prioritize explicit language match if searching in English (#8946) --- .../Extensions/EnumerableExtensions.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs index a5a6b18aa8..c6d1f3900a 100644 --- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs +++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs @@ -24,24 +24,27 @@ namespace MediaBrowser.Model.Extensions requestedLanguage = "en"; } - var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase); - return remoteImageInfos.OrderByDescending(i => { + // Image priority ordering: + // - Images that match the requested language + // - Images with no language + // - TODO: Images that match the original language + // - Images in English + // - Images that don't match the requested language + if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) { - return 3; + return 4; } if (string.IsNullOrEmpty(i.Language)) { - // Assume empty image language is likely to be English. - return isRequestedLanguageEn ? 3 : 2; + return 3; } - if (!isRequestedLanguageEn && string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase)) { - // Prioritize English over non-requested languages. return 2; } From 1f658f59b8a6a5e110d9ee70932e5a91f2e0845e Mon Sep 17 00:00:00 2001 From: xdo <35262360+Xavier-Do@users.noreply.github.com> Date: Fri, 30 Dec 2022 03:40:24 +0100 Subject: [PATCH 21/97] Fix multi cleaning (#8978) Right now, a movie Name `Iron Man Multi 1080p.mkv` will be searched as `Iron Man Multi` leading to no result. The cleaning regex was containing multi but it looks like a typo joined `multi` and `subs` in the same term. Co-authored-by: Xavier-Do --- Emby.Naming/Common/NamingOptions.cs | 2 +- tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 0119fa38c8..03fa65b4eb 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -153,7 +153,7 @@ namespace Emby.Naming.Common CleanStrings = new[] { - @"^\s*(?.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)", + @"^\s*(?.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"^(?.+?)(\[.*\])", @"^\s*(?.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?.+)", diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs index 1574bce581..6c9c98cbe8 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs @@ -10,6 +10,7 @@ namespace Jellyfin.Naming.Tests.Video [Theory] [InlineData("Super movie 480p.mp4", "Super movie")] + [InlineData("Super movie Multi.mp4", "Super movie")] [InlineData("Super movie 480p 2001.mp4", "Super movie")] [InlineData("Super movie [480p].mp4", "Super movie")] [InlineData("480 Super movie [tmdbid=12345].mp4", "480 Super movie")] From 832634913798be14482479414236b69ce7f7ca78 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Thu, 29 Dec 2022 19:40:39 -0700 Subject: [PATCH 22/97] Use custom database healthcheck (#8973) --- .../DbContextFactoryHealthCheck.cs | 43 +++++++++++++++++++ Jellyfin.Server/Startup.cs | 3 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs diff --git a/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs b/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs new file mode 100644 index 0000000000..bf00dcd53f --- /dev/null +++ b/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Jellyfin.Server.HealthChecks; + +/// +/// Implementation of the for a . +/// +/// The type of database context. +public class DbContextFactoryHealthCheck : IHealthCheck + where TContext : DbContext +{ + private readonly IDbContextFactory _dbContextFactory; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public DbContextFactoryHealthCheck(IDbContextFactory contextFactory) + { + _dbContextFactory = contextFactory; + } + + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(context); + + var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + if (await dbContext.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false)) + { + return HealthCheckResult.Healthy(); + } + } + + return HealthCheckResult.Unhealthy(); + } +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 49a57aa688..5d6a278c40 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -8,6 +8,7 @@ using System.Text; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; +using Jellyfin.Server.HealthChecks; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Infrastructure; @@ -122,7 +123,7 @@ namespace Jellyfin.Server .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); services.AddHealthChecks() - .AddDbContextCheck(); + .AddCheck>(nameof(JellyfinDb)); services.AddHlsPlaylistGenerator(); } From 431919301d7a2a81dda53afd16012a74f8073db9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 30 Dec 2022 06:33:45 -0700 Subject: [PATCH 23/97] chore(deps): update dependency moq to v4.18.4 (#8986) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../Emby.Server.Implementations.Fuzz.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj index 6cc814ef48..9c2449da4d 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj +++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj @@ -18,7 +18,7 @@ - + From c73c7b60506e61638f70c69506e58353e06f4a41 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Fri, 30 Dec 2022 16:08:02 +0100 Subject: [PATCH 24/97] Checkout common ancestor for comparison in OpenAPI workflow (#8985) --- .github/workflows/openapi.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index f426357a9a..5dee03ef88 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -41,7 +41,15 @@ jobs: - name: Checkout repository uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 with: - ref: ${{ github.base_ref }} + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 0 + - name: Checkout common ancestor + run: | + git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }} + git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/* + ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }}) + git checkout --progress --force $ANCESTOR_REF - name: Setup .NET uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 with: From 9a740344be757c2ab9a3f434295622573c39b181 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Fri, 30 Dec 2022 08:24:06 -0700 Subject: [PATCH 25/97] Add support for .sup subtitle (#8808) Fixes https://github.com/jellyfin/jellyfin/issues/8628 --- Emby.Dlna/Profiles/DefaultProfile.cs | 12 ++++++++++++ Emby.Naming/Common/NamingOptions.cs | 1 + MediaBrowser.Controller/Entities/BaseItem.cs | 1 + .../MediaEncoding/EncodingHelper.cs | 4 +++- MediaBrowser.Model/Entities/MediaStream.cs | 11 ++++++----- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs index 23437f1bdf..54a0a87a89 100644 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -92,6 +92,12 @@ namespace Emby.Dlna.Profiles Method = SubtitleDeliveryMethod.External, }, + new SubtitleProfile + { + Format = "sup", + Method = SubtitleDeliveryMethod.External + }, + new SubtitleProfile { Format = "srt", @@ -140,6 +146,12 @@ namespace Emby.Dlna.Profiles Method = SubtitleDeliveryMethod.Embed }, + new SubtitleProfile + { + Format = "sup", + Method = SubtitleDeliveryMethod.Embed + }, + new SubtitleProfile { Format = "subrip", diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 03fa65b4eb..54f62a1570 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -169,6 +169,7 @@ namespace Emby.Naming.Common ".srt", ".ssa", ".sub", + ".sup", ".vtt", }; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 49dd151f36..f2c2007f7a 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -56,6 +56,7 @@ namespace MediaBrowser.Controller.Entities ".srt", ".vtt", ".sub", + ".sup", ".idx", ".txt", ".edl", diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 7264c5eed0..b40c224d5d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -938,8 +938,10 @@ namespace MediaBrowser.Controller.MediaEncoding && state.SubtitleStream.IsExternal) { var subtitlePath = state.SubtitleStream.Path; + var subtitleExtension = Path.GetExtension(subtitlePath); - if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase) + || string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase)) { var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); if (File.Exists(idxFile)) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 344ebaf808..47341f4e17 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -635,11 +635,12 @@ namespace MediaBrowser.Model.Entities // sub = external .sub file - return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) && - !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase) && - !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) && - !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase) && - !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase); + return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) + && !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase) + && !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) + && !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase) + && !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase) + && !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase); } public bool SupportsSubtitleConversionTo(string toCodec) From cfda11d5f987de8bfc4e3f2cf47548977f86cf77 Mon Sep 17 00:00:00 2001 From: Xavier Rosell Date: Fri, 30 Dec 2022 10:27:14 +0000 Subject: [PATCH 26/97] Translated using Weblate (Catalan) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/ --- Emby.Server.Implementations/Localization/Core/ca.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 50c574d674..1966f69683 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -40,7 +40,7 @@ "Movies": "Pel·lícules", "Music": "Música", "MusicVideos": "Vídeos Musicals", - "NameInstallFailed": "Instal·lació de {0} fallida", + "NameInstallFailed": "{0} instal·lació fallida", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada Desconeguda", "NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.", From 2fbdc0c1638d0e750cae43cc44fea10414e887c0 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 31 Dec 2022 15:42:24 +0100 Subject: [PATCH 27/97] Give stale action write permission for issues --- .github/workflows/repo-stale.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index c1f5e718b8..897b7014ac 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -5,7 +5,8 @@ on: - cron: '30 1 * * *' workflow_dispatch: -permissions: {} +permissions: + issues: write jobs: stale: runs-on: ubuntu-latest @@ -22,7 +23,7 @@ jobs: stale-issue-label: stale stale-issue-message: |- This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments. - + If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. - + This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html). From 6e7b0a8cb2d2807855bcef986a02de111bc82731 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Mon, 2 Jan 2023 11:07:00 +0100 Subject: [PATCH 28/97] Use nfo provided remote images on initial scan --- MediaBrowser.Providers/Manager/MetadataService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 5b5ca0fca1..8e5d16bafa 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -685,7 +685,8 @@ namespace MediaBrowser.Providers.Manager { try { - if (!options.IsReplacingImage(remoteImage.Type)) + if (item.ImageInfos.Any(x => x.Type == remoteImage.Type) + && !options.IsReplacingImage(remoteImage.Type)) { continue; } From bb7bf8378b546614e7e9d1cef99f19150d744ef2 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Wed, 4 Jan 2023 04:29:38 +0800 Subject: [PATCH 29/97] Add jellyfin to the render and video groups for HWA Signed-off-by: nyanmisaka --- debian/postinst | 10 ++++++++++ fedora/jellyfin.spec | 3 +++ 2 files changed, 13 insertions(+) diff --git a/debian/postinst b/debian/postinst index 47173855f7..a15442c76e 100644 --- a/debian/postinst +++ b/debian/postinst @@ -10,6 +10,8 @@ if [[ -f $DEFAULT_FILE ]]; then fi JELLYFIN_USER=${JELLYFIN_USER:-jellyfin} +RENDER_GROUP=${RENDER_GROUP:-render} +VIDEO_GROUP=${VIDEO_GROUP:-video} # Data directories for program data (cache, db), configs, and logs PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME} @@ -28,6 +30,14 @@ case "$1" in adduser --system --ingroup ${JELLYFIN_USER} --shell /bin/false ${JELLYFIN_USER} --no-create-home --home ${PROGRAMDATA} \ --gecos "Jellyfin default user" > /dev/null 2>&1 fi + # add jellyfin to the render group for hwa + if [[ ! -z "$(getent group ${RENDER_GROUP})" ]]; then + usermod -aG ${RENDER_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1 + fi + # add jellyfin to the video group for hwa + if [[ ! -z "$(getent group ${VIDEO_GROUP})" ]]; then + usermod -aG ${VIDEO_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1 + fi # ensure $PROGRAMDATA exists if [[ ! -d $PROGRAMDATA ]]; then mkdir $PROGRAMDATA diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index 416d883607..08de715370 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -139,6 +139,9 @@ getent group jellyfin >/dev/null || groupadd -r jellyfin getent passwd jellyfin >/dev/null || \ useradd -r -g jellyfin -d %{_sharedstatedir}/jellyfin -s /sbin/nologin \ -c "Jellyfin default user" jellyfin +# Add jellyfin to the render and video groups for hwa. +[ ! -z "$(getent group render)" ] && usermod -aG render jellyfin >/dev/null 2>&1 +[ ! -z "$(getent group video)" ] && usermod -aG video jellyfin >/dev/null 2>&1 exit 0 %post server From 69a51c425acef65c229e95f2c361226bd81d64c5 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 4 Jan 2023 16:01:31 +0100 Subject: [PATCH 30/97] Fix all warnings in Jellyfin.Api (#9003) --- .../Attributes/AcceptsFileAttribute.cs | 2 +- .../Attributes/ProducesFileAttribute.cs | 2 +- Jellyfin.Api/BaseJellyfinApiController.cs | 18 --- Jellyfin.Api/Controllers/ApiKeyController.cs | 2 +- Jellyfin.Api/Controllers/ImageController.cs | 151 ++++++++++-------- Jellyfin.Api/Controllers/LiveTvController.cs | 3 +- Jellyfin.Api/Controllers/PackageController.cs | 4 +- Jellyfin.Api/Controllers/PluginsController.cs | 3 +- .../Controllers/SubtitleController.cs | 40 +++-- .../Controllers/SyncPlayController.cs | 3 +- Jellyfin.Api/Controllers/SystemController.cs | 3 +- .../Controllers/UserLibraryController.cs | 2 +- Jellyfin.Api/Jellyfin.Api.csproj | 4 - .../LiveTvDtos/ChannelMappingOptionsDto.cs | 6 +- Jellyfin.Server/Filters/FileRequestFilter.cs | 2 +- Jellyfin.Server/Filters/FileResponseFilter.cs | 2 +- .../Routines/AddDefaultPluginRepository.cs | 2 +- .../Routines/ReaddDefaultPluginRepository.cs | 4 +- .../Configuration/ServerConfiguration.cs | 2 +- jellyfin.ruleset | 4 + 20 files changed, 128 insertions(+), 131 deletions(-) diff --git a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs index 58552d847d..fbe68b6b97 100644 --- a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs +++ b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs @@ -25,6 +25,6 @@ namespace Jellyfin.Api.Attributes /// Gets the configured content types. /// /// the configured content types. - public string[] GetContentTypes() => _contentTypes; + public string[] ContentTypes => _contentTypes; } } diff --git a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs index 2bf77d729a..d8e4141acb 100644 --- a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs @@ -25,6 +25,6 @@ namespace Jellyfin.Api.Attributes /// Gets the configured content types. /// /// the configured content types. - public string[] GetContentTypes() => _contentTypes; + public string[] ContentTypes => _contentTypes; } } diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index 0c63d24b70..e327831fe7 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -17,24 +17,6 @@ namespace Jellyfin.Api JsonDefaults.PascalCaseMediaType)] public class BaseJellyfinApiController : ControllerBase { - /// - /// Create a new . - /// - /// The value to return. - /// The type to return. - /// The . - protected ActionResult> Ok(List value) - => new OkResult>(value); - - /// - /// Create a new . - /// - /// The value to return. - /// The type to return. - /// The . - protected ActionResult> Ok(IReadOnlyList value) - => new OkResult>(value); - /// /// Create a new . /// diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 593846adc9..024a15349e 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetKeys() { - var keys = await _authenticationManager.GetApiKeys(); + var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false); return new QueryResult(keys); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 49342ad5ce..534667c8c6 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -106,24 +106,26 @@ namespace Jellyfin.Api.Controllers } var user = _userManager.GetUserById(userId); - await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - - // Handle image/png; charset=utf-8 - var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); - if (user.ProfileImage is not null) + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) { - await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); + var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + if (user.ProfileImage is not null) + { + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + } + + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); + + await _providerManager + .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .ConfigureAwait(false); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + return NoContent(); } - - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); - - await _providerManager - .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) - .ConfigureAwait(false); - await _userManager.UpdateUserAsync(user).ConfigureAwait(false); - - return NoContent(); } /// @@ -153,24 +155,26 @@ namespace Jellyfin.Api.Controllers } var user = _userManager.GetUserById(userId); - await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - - // Handle image/png; charset=utf-8 - var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); - if (user.ProfileImage is not null) + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) { - await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); + var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + if (user.ProfileImage is not null) + { + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + } + + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); + + await _providerManager + .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .ConfigureAwait(false); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + return NoContent(); } - - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); - - await _providerManager - .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) - .ConfigureAwait(false); - await _userManager.UpdateUserAsync(user).ConfigureAwait(false); - - return NoContent(); } /// @@ -341,14 +345,16 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) + { + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); + await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); - // Handle image/png; charset=utf-8 - var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); - await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); - - return NoContent(); + return NoContent(); + } } /// @@ -377,14 +383,16 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) + { + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); + await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); - // Handle image/png; charset=utf-8 - var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); - await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); - - return NoContent(); + return NoContent(); + } } /// @@ -1788,32 +1796,35 @@ namespace Jellyfin.Api.Controllers [AcceptsImageFile] public async Task UploadCustomSplashscreen() { - await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - - var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; - - if (!mimeType.HasValue) + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) { - return BadRequest("Error reading mimetype from uploaded image"); + var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; + + if (!mimeType.HasValue) + { + return BadRequest("Error reading mimetype from uploaded image"); + } + + var extension = MimeTypes.ToExtension(mimeType.Value); + if (string.IsNullOrEmpty(extension)) + { + return BadRequest("Error converting mimetype to an image extension"); + } + + var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); + var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); + brandingOptions.SplashscreenLocation = filePath; + _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); + + var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await using (fs.ConfigureAwait(false)) + { + await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); + } + + return NoContent(); } - - var extension = MimeTypes.ToExtension(mimeType.Value); - if (string.IsNullOrEmpty(extension)) - { - return BadRequest("Error converting mimetype to an image extension"); - } - - var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); - var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); - brandingOptions.SplashscreenLocation = filePath; - _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); - - await using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) - { - await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); - } - - return NoContent(); } /// diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 94710d78f2..5228e0babf 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1011,10 +1011,9 @@ namespace Jellyfin.Api.Controllers { if (!string.IsNullOrEmpty(pw)) { - using var sha = SHA1.Create(); // TODO: remove ToLower when Convert.ToHexString supports lowercase // Schedules Direct requires the hex to be lowercase - listingsProviderInfo.Password = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant(); + listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant(); } return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 0aa7c2ac9e..10f967dcde 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -145,7 +145,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetRepositories() { - return _serverConfigurationManager.Configuration.PluginRepositories; + return Ok(_serverConfigurationManager.Configuration.PluginRepositories.AsEnumerable()); } /// @@ -157,7 +157,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Repositories")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SetRepositories([FromBody, Required] List repositoryInfos) + public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos) { _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos; _serverConfigurationManager.SaveConfiguration(); diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 6a729b2373..b8a09990a5 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json; @@ -143,7 +144,7 @@ namespace Jellyfin.Api.Controllers public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) { // If no version is given, return the current instance. - var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)); + var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList(); // Select the un-instanced one first. var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index ff9bd095b5..c3ce1868e2 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -236,14 +236,17 @@ namespace Jellyfin.Api.Controllers if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) { - await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); - using var reader = new StreamReader(stream); + Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + using var reader = new StreamReader(stream); - var text = await reader.ReadToEndAsync().ConfigureAwait(false); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); - text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal); + text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal); - return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format)); + return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format)); + } } return File( @@ -403,19 +406,22 @@ namespace Jellyfin.Api.Controllers { var video = (Video)_libraryManager.GetItemById(itemId); var data = Convert.FromBase64String(body.Data); - await using var memoryStream = new MemoryStream(data); - await _subtitleManager.UploadSubtitle( - video, - new SubtitleResponse - { - Format = body.Format, - Language = body.Language, - IsForced = body.IsForced, - Stream = memoryStream - }).ConfigureAwait(false); - _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); + var memoryStream = new MemoryStream(data, 0, data.Length, false, true); + await using (memoryStream.ConfigureAwait(false)) + { + await _subtitleManager.UploadSubtitle( + video, + new SubtitleResponse + { + Format = body.Format, + Language = body.Language, + IsForced = body.IsForced, + Stream = memoryStream + }).ConfigureAwait(false); + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); - return NoContent(); + return NoContent(); + } } /// diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index e194fc556e..99347246e0 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -107,7 +108,7 @@ namespace Jellyfin.Api.Controllers { var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new ListGroupsRequest(); - return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest)); + return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable()); } /// diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 411c987f39..2d594293e0 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -216,8 +216,7 @@ namespace Jellyfin.Api.Controllers public ActionResult> GetWakeOnLanInfo() { var result = _network.GetMacAddresses() - .Select(i => new WakeOnLanInfo(i)) - .ToList(); + .Select(i => new WakeOnLanInfo(i)); return Ok(result); } } diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index c18fa29af6..cd21c5f6ff 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -211,7 +211,7 @@ namespace Jellyfin.Api.Controllers if (item is IHasTrailers hasTrailers) { var trailers = hasTrailers.LocalTrailers; - return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item)); + return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable()); } return Ok(item.GetExtras() diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 889f7dc9ab..eca68c6dfa 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -12,10 +12,6 @@ AD0001 - - false - - diff --git a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs index f43822da77..e293c461cf 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs @@ -14,14 +14,12 @@ namespace Jellyfin.Api.Models.LiveTvDtos /// /// Gets or sets list of tuner channels. /// - [SuppressMessage("Microsoft.Performance", "CA2227:ReadOnlyRemoveSetter", MessageId = "TunerChannels", Justification = "Imported from ServiceStack")] - public List TunerChannels { get; set; } = null!; + required public IReadOnlyList TunerChannels { get; set; } /// /// Gets or sets list of provider channels. /// - [SuppressMessage("Microsoft.Performance", "CA2227:ReadOnlyRemoveSetter", MessageId = "ProviderChannels", Justification = "Imported from ServiceStack")] - public List ProviderChannels { get; set; } = null!; + required public IReadOnlyList ProviderChannels { get; set; } /// /// Gets or sets list of mappings. diff --git a/Jellyfin.Server/Filters/FileRequestFilter.cs b/Jellyfin.Server/Filters/FileRequestFilter.cs index 69e10994f4..bb5d6a4123 100644 --- a/Jellyfin.Server/Filters/FileRequestFilter.cs +++ b/Jellyfin.Server/Filters/FileRequestFilter.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters { if (attribute is AcceptsFileAttribute acceptsFileAttribute) { - operation.RequestBody = GetRequestBody(acceptsFileAttribute.GetContentTypes()); + operation.RequestBody = GetRequestBody(acceptsFileAttribute.ContentTypes); break; } } diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs index 544fdbfd63..1a4559d260 100644 --- a/Jellyfin.Server/Filters/FileResponseFilter.cs +++ b/Jellyfin.Server/Filters/FileResponseFilter.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Server.Filters response.Value.Content.Clear(); // Add all content-types as file. - foreach (var contentType in producesFileAttribute.GetContentTypes()) + foreach (var contentType in producesFileAttribute.ContentTypes) { response.Value.Content.Add(contentType, _openApiMediaType); } diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs index f6d8c9cc0d..9e12c2e6bd 100644 --- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs +++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs @@ -38,7 +38,7 @@ namespace Jellyfin.Server.Migrations.Routines /// public void Perform() { - _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo); + _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo }; _serverConfigurationManager.SaveConfiguration(); } } diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs index 394f14d63c..9cfaec46f8 100644 --- a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs +++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs @@ -39,9 +39,9 @@ namespace Jellyfin.Server.Migrations.Routines public void Perform() { // Only add if repository list is empty - if (_serverConfigurationManager.Configuration.PluginRepositories.Count == 0) + if (_serverConfigurationManager.Configuration.PluginRepositories.Length == 0) { - _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo); + _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo }; _serverConfigurationManager.SaveConfiguration(); } } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index a07ab7121e..d3e042abaa 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -194,7 +194,7 @@ namespace MediaBrowser.Model.Configuration public string[] CodecsUsed { get; set; } = Array.Empty(); - public List PluginRepositories { get; set; } = new List(); + public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty(); public bool EnableExternalContentInSuggestions { get; set; } = true; diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 71385cee2a..3fda774b8d 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -138,6 +138,10 @@ + + + + From a07b14057ff40675e2b8bba54e17dc9cb710ed64 Mon Sep 17 00:00:00 2001 From: Futoshi Iwashita Date: Wed, 4 Jan 2023 12:55:38 +0000 Subject: [PATCH 31/97] Translated using Weblate (Japanese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/ --- Emby.Server.Implementations/Localization/Core/ja.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index d90d705b2e..7f616c35ad 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "データベースの最適化", "TaskKeyframeExtractorDescription": "より正確なHLSプレイリストを作成するため、動画ファイルからキーフレームを抽出する。この処理には時間がかかる場合があります。", "TaskKeyframeExtractor": "キーフレーム抽出", - "External": "外部" + "External": "外部", + "HearingImpaired": "聴覚障害の方" } From c795f17fa6ae9e4f68f23cb569a1129de8fd7635 Mon Sep 17 00:00:00 2001 From: Porrumentzio Date: Tue, 3 Jan 2023 21:19:08 +0000 Subject: [PATCH 32/97] Translated using Weblate (Basque) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eu/ --- Emby.Server.Implementations/Localization/Core/eu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json index d657ac7b69..e91084f920 100644 --- a/Emby.Server.Implementations/Localization/Core/eu.json +++ b/Emby.Server.Implementations/Localization/Core/eu.json @@ -100,7 +100,7 @@ "ItemRemovedWithName": "{0} liburutegitik ezabatu da", "ItemAddedWithName": "{0} liburutegira gehitu da", "HomeVideos": "Etxeko bideoak", - "HeaderNextUp": "Hurrengoa", + "HeaderNextUp": "Nobedadeak", "HeaderLiveTV": "Zuzeneko TB", "HeaderFavoriteSongs": "Gogoko abestiak", "HeaderFavoriteShows": "Gogoko showak", From d817b54fe794cd45c2a231c73493088e0a904dbe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 00:33:15 +0000 Subject: [PATCH 33/97] chore(deps): update actions/download-artifact digest to 9bc31d5 --- .github/workflows/openapi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 5dee03ef88..f27d5bed67 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -76,12 +76,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3 with: name: openapi-base path: openapi-base From f0e0080387c66a9e7e6787e8649164de2bdab86e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 17:02:45 +0000 Subject: [PATCH 34/97] chore(deps): update actions/upload-artifact digest to 0b7f8ab --- .github/workflows/openapi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 5dee03ef88..733798f1ec 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -25,7 +25,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3 + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3 with: name: openapi-head retention-days: 14 @@ -57,7 +57,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3 + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3 with: name: openapi-base retention-days: 14 From 769c48c629bae859ba713d66b1a55f35aca708b6 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 7 Jan 2023 19:30:55 +0100 Subject: [PATCH 35/97] Deduplicate media stream ordering code (#9014) --- .../Library/MediaStreamSelector.cs | 54 ++++------------ jellyfin.ruleset | 2 + .../Library/MediaStreamSelectorTests.cs | 64 +++++++++++++++++++ 3 files changed, 78 insertions(+), 42 deletions(-) diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 74c53b2dac..6aef87c525 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -89,17 +89,7 @@ namespace Emby.Server.Implementations.Library // Give some preference to external text subs for better performance return streams .Where(i => i.Type == type) - .OrderBy(i => - { - var index = languagePreferences.FindIndex(x => string.Equals(x, i.Language, StringComparison.OrdinalIgnoreCase)); - - return index == -1 ? 100 : index; - }) - .ThenBy(i => GetBooleanOrderBy(i.IsDefault)) - .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream)) - .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream)) - .ThenBy(i => GetBooleanOrderBy(i.IsExternal)) - .ThenBy(i => i.Index); + .OrderByDescending(i => GetStreamScore(i, languagePreferences)); } public static void SetSubtitleStreamScores( @@ -113,9 +103,9 @@ namespace Emby.Server.Implementations.Library return; } - var sortedStreams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages); + var sortedStreams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages).ToList(); - var filteredStreams = new List(); + List? filteredStreams = null; if (mode == SubtitlePlaybackMode.Default) { @@ -144,46 +134,26 @@ namespace Emby.Server.Implementations.Library } // load forced subs if we have found no suitable full subtitles - var iterStreams = filteredStreams.Count == 0 + var iterStreams = filteredStreams is null || filteredStreams.Count == 0 ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) : filteredStreams; foreach (var stream in iterStreams) { - stream.Score = GetSubtitleScore(stream, preferredLanguages); + stream.Score = GetStreamScore(stream, preferredLanguages); } } - private static int GetSubtitleScore(MediaStream stream, IReadOnlyList languagePreferences) + internal static int GetStreamScore(MediaStream stream, IReadOnlyList languagePreferences) { - var values = new List(); - var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase)); - - values.Add(index == -1 ? 0 : 100 - index); - - values.Add(stream.IsForced ? 1 : 0); - values.Add(stream.IsDefault ? 1 : 0); - values.Add(stream.SupportsExternalStream ? 1 : 0); - values.Add(stream.IsTextSubtitleStream ? 1 : 0); - values.Add(stream.IsExternal ? 1 : 0); - - values.Reverse(); - var scale = 1; - var score = 0; - - foreach (var value in values) - { - score += scale * (value + 1); - scale *= 10; - } - + var score = index == -1 ? 1 : 101 - index; + score = (score * 10) + (stream.IsForced ? 2 : 1); + score = (score * 10) + (stream.IsDefault ? 2 : 1); + score = (score * 10) + (stream.SupportsExternalStream ? 2 : 1); + score = (score * 10) + (stream.IsTextSubtitleStream ? 2 : 1); + score = (score * 10) + (stream.IsExternal ? 2 : 1); return score; } - - private static int GetBooleanOrderBy(bool value) - { - return value ? 0 : 1; - } } } diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 3fda774b8d..b611caa11a 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -93,6 +93,8 @@ + + diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs index 538010f6c0..07feae587b 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs @@ -51,4 +51,68 @@ public class MediaStreamSelectorTests Assert.Equal(expectedIndex, MediaStreamSelector.GetDefaultAudioStreamIndex(streams, preferredLanguages, preferDefaultTrack)); } + + public static TheoryData GetStreamScore_MediaStream_TestData() + { + var data = new TheoryData(); + + data.Add(new MediaStream(), 111111); + data.Add( + new MediaStream() + { + Language = "eng" + }, + 10111111); + data.Add( + new MediaStream() + { + Language = "fre" + }, + 10011111); + data.Add( + new MediaStream() + { + IsForced = true + }, + 121111); + data.Add( + new MediaStream() + { + IsDefault = true + }, + 112111); + data.Add( + new MediaStream() + { + SupportsExternalStream = true + }, + 111211); + data.Add( + new MediaStream() + { + IsExternal = true + }, + 111112); + data.Add( + new MediaStream() + { + Language = "eng", + IsForced = true, + IsDefault = true, + SupportsExternalStream = true, + IsExternal = true + }, + 10122212); + + return data; + } + + [Theory] + [MemberData(nameof(GetStreamScore_MediaStream_TestData))] + public void GetStreamScore_MediaStream_CorrectScore(MediaStream stream, int expectedScore) + { + var languagePref = new[] { "eng", "fre" }; + + Assert.Equal(expectedScore, MediaStreamSelector.GetStreamScore(stream, languagePref)); + } } From 678bcf9a80ef1fcf7928df6e94030028b4d6c1af Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sat, 7 Jan 2023 11:31:10 -0700 Subject: [PATCH 36/97] Use EventManager for AuthenticationSuccess, AuthenticationFailure (#8960) --- Emby.Server.Implementations/Session/SessionManager.cs | 11 ++--------- Jellyfin.Server.Implementations/Users/UserManager.cs | 4 +++- MediaBrowser.Controller/Session/ISessionManager.cs | 10 ---------- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 2f60d01a92..afa3721b88 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -95,12 +95,6 @@ namespace Emby.Server.Implementations.Session _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated; } - /// - public event EventHandler> AuthenticationFailed; - - /// - public event EventHandler> AuthenticationSucceeded; - /// /// Occurs when playback has started. /// @@ -1468,7 +1462,7 @@ namespace Emby.Server.Implementations.Session if (user is null) { - AuthenticationFailed?.Invoke(this, new GenericEventArgs(request)); + await _eventManager.PublishAsync(new GenericEventArgs(request)).ConfigureAwait(false); throw new AuthenticationException("Invalid username or password entered."); } @@ -1504,8 +1498,7 @@ namespace Emby.Server.Implementations.Session ServerId = _appHost.SystemId }; - AuthenticationSucceeded?.Invoke(this, new GenericEventArgs(returnResult)); - + await _eventManager.PublishAsync(new GenericEventArgs(returnResult)).ConfigureAwait(false); return returnResult; } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index ae3fcad29b..19ac007b93 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -157,7 +157,9 @@ namespace Jellyfin.Server.Implementations.Users await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); } - OnUserUpdated?.Invoke(this, new GenericEventArgs(user)); + var eventArgs = new UserUpdatedEventArgs(user); + await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); + OnUserUpdated?.Invoke(this, eventArgs); } /// diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index b16399598c..eefc5d222e 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -57,16 +57,6 @@ namespace MediaBrowser.Controller.Session /// event EventHandler CapabilitiesChanged; - /// - /// Occurs when [authentication failed]. - /// - event EventHandler> AuthenticationFailed; - - /// - /// Occurs when [authentication succeeded]. - /// - event EventHandler> AuthenticationSucceeded; - /// /// Gets the sessions. /// From ac9b7142cc0b479c9123b149521c1cad969623a1 Mon Sep 17 00:00:00 2001 From: Brad Beattie Date: Sat, 7 Jan 2023 10:31:33 -0800 Subject: [PATCH 37/97] Fixing similar parental rating calculation (#8959) --- .../Data/SqliteItemRepository.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 9bdc4e5c8e..763ff77f10 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2401,13 +2401,17 @@ namespace Emby.Server.Implementations.Data var builder = new StringBuilder(); builder.Append('('); - if (string.IsNullOrEmpty(item.OfficialRating)) + if (item.InheritedParentalRatingValue == 0) { - builder.Append("(OfficialRating is null * 10)"); + builder.Append("((InheritedParentalRatingValue=0) * 10)"); } else { - builder.Append("(OfficialRating=@ItemOfficialRating * 10)"); + builder.Append( + @"(SELECT CASE WHEN InheritedParentalRatingValue=0 + THEN 0 + ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue)) + END)"); } if (item.ProductionYear.HasValue) @@ -2521,6 +2525,11 @@ namespace Emby.Server.Implementations.Data { statement.TryBind("@SimilarItemId", item.Id); } + + if (commandText.Contains("@InheritedParentalRatingValue", StringComparison.OrdinalIgnoreCase)) + { + statement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); + } } private string GetJoinUserDataText(InternalItemsQuery query) From a2b792e38697d8b11453254c77be62a2116a6e6a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 11:32:19 -0700 Subject: [PATCH 38/97] chore(deps): update actions/checkout digest to ac59398 (#9015) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/openapi.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5aebbae4d6..7153d4cf5f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 - name: Setup .NET uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index f62ae853dd..5d945c001b 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 1130703e88..4577ff5251 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} From 46e9f5ad2e1f61cefe719d03f5a8986434fbb7b6 Mon Sep 17 00:00:00 2001 From: Egor Bakanov <33174871+EgorBakanov@users.noreply.github.com> Date: Sun, 8 Jan 2023 01:48:14 +0700 Subject: [PATCH 39/97] Fix recursive children lookup of folders (#8678) Fixes https://github.com/jellyfin/jellyfin/issues/6193 Fixes https://github.com/jellyfin/jellyfin/issues/7226 --- CONTRIBUTORS.md | 1 + MediaBrowser.Controller/Entities/Folder.cs | 64 +++++++++++----------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8daaae4d94..42242c51a0 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -37,6 +37,7 @@ - [DMouse10462](https://github.com/DMouse10462) - [DrPandemic](https://github.com/DrPandemic) - [eglia](https://github.com/eglia) + - [EgorBakanov](https://github.com/EgorBakanov) - [EraYaN](https://github.com/EraYaN) - [escabe](https://github.com/escabe) - [excelite](https://github.com/excelite) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index e586205c32..bccb4107ff 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1300,8 +1300,15 @@ namespace MediaBrowser.Controller.Entities /// /// Adds the children to list. /// - private void AddChildren(User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query) + private void AddChildren(User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query, HashSet visitedFolders = null) { + // Prevent infinite recursion of nested folders + visitedFolders ??= new HashSet(); + if (!visitedFolders.Add(this)) + { + return; + } + // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums. IEnumerable children = null; if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum)) @@ -1316,42 +1323,33 @@ namespace MediaBrowser.Controller.Entities children = GetEligibleChildrenForRecursiveChildren(user); } - foreach (var child in children) - { - bool? isVisibleToUser = null; - - if (query is null || UserViewBuilder.FilterItem(child, query)) - { - isVisibleToUser = child.IsVisible(user); - - if (isVisibleToUser.Value) - { - result[child.Id] = child; - } - } - - if (isVisibleToUser ?? child.IsVisible(user)) - { - if (recursive && child.IsFolder) - { - var folder = (Folder)child; - - folder.AddChildren(user, includeLinkedChildren, result, true, query); - } - } - } + AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders); if (includeLinkedChildren) { - foreach (var child in GetLinkedChildren(user)) + AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query, visitedFolders); + } + } + + private void AddChildrenFromCollection(IEnumerable children, User user, bool includeLinkedChildren, Dictionary result, bool recursive, InternalItemsQuery query, HashSet visitedFolders) + { + foreach (var child in children) + { + if (!child.IsVisible(user)) { - if (query is null || UserViewBuilder.FilterItem(child, query)) - { - if (child.IsVisible(user)) - { - result[child.Id] = child; - } - } + continue; + } + + if (query is null || UserViewBuilder.FilterItem(child, query)) + { + result[child.Id] = child; + } + + if (recursive && child.IsFolder) + { + var folder = (Folder)child; + + folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders); } } } From 4eeb522144037d9102b63478d7d5c0195a1ceef8 Mon Sep 17 00:00:00 2001 From: David Fairbrother Date: Sat, 7 Jan 2023 19:25:24 +0000 Subject: [PATCH 40/97] Add dts to list of audio codecs which require strict -2 Adds dts to the list of audio codecs where ffmpeg will throw asking us to opt into experimental support. This is seen when the original content is based on dts and we don't acopy using ffmpeg. --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index af43bb578e..5f5f34b0d7 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1704,11 +1704,12 @@ namespace Jellyfin.Api.Controllers return audioTranscodeParams; } - // flac and opus are experimental in mp4 muxer + // dts, flac and opus are experimental in mp4 muxer var strictArgs = string.Empty; if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)) + || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)) { strictArgs = " -strict -2"; } From abeadc62daccd5d0df3b4d099df4e718f27b21ad Mon Sep 17 00:00:00 2001 From: David Fairbrother Date: Sat, 7 Jan 2023 19:33:57 +0000 Subject: [PATCH 41/97] Add DavidFair to contributors Add myself to the list of contributors, as per the development guidelines found on the JF website. --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 42242c51a0..ec3c6fd2af 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -27,6 +27,7 @@ - [cvium](https://github.com/cvium) - [dannymichel](https://github.com/dannymichel) - [DaveChild](https://github.com/DaveChild) + - [DavidFair](https://github.com/DavidFair) - [Delgan](https://github.com/Delgan) - [dcrdev](https://github.com/dcrdev) - [dhartung](https://github.com/dhartung) From 34fe2d00a2ccca462666b56f677922e4922d8ed7 Mon Sep 17 00:00:00 2001 From: Retrial Date: Fri, 6 Jan 2023 19:09:28 +0000 Subject: [PATCH 42/97] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- .../Localization/Core/el.json | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 8e9287af48..c6e2244cae 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -15,7 +15,7 @@ "Favorites": "Αγαπημένα", "Folders": "Φάκελοι", "Genres": "Είδη", - "HeaderAlbumArtists": "Δισκογραφικοί καλλιτέχνες", + "HeaderAlbumArtists": "Καλλιτέχνες άλμπουμ", "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση", "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ", "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες", @@ -24,8 +24,8 @@ "HeaderFavoriteSongs": "Αγαπημένα Τραγούδια", "HeaderLiveTV": "Ζωντανή Τηλεόραση", "HeaderNextUp": "Επόμενο", - "HeaderRecordingGroups": "Μουσικά Συγκροτήματα", - "HomeVideos": "Προσωπικά βίντεο", + "HeaderRecordingGroups": "Ομάδες Ηχογράφησης", + "HomeVideos": "Προσωπικά Βίντεο", "Inherit": "Κληρονόμηση", "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη", @@ -51,10 +51,10 @@ "NotificationOptionCameraImageUploaded": "Μεταφορτώθηκε φωτογραφία απο κάμερα", "NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης", "NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο", - "NotificationOptionPluginError": "Αποτυχία του plugin", - "NotificationOptionPluginInstalled": "Το plugin εγκαταστάθηκε", - "NotificationOptionPluginUninstalled": "Το plugin απεγκαταστάθηκε", - "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του plugin εγκαταστάθηκε", + "NotificationOptionPluginError": "Αποτυχία του πρόσθετου", + "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε", + "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε", + "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε", "NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση", "NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας", "NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε", @@ -66,7 +66,7 @@ "PluginInstalledWithName": "{0} εγκαταστήθηκε", "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί", "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί", - "ProviderValue": "Provider: {0}", + "ProviderValue": "Πάροχος: {0}", "ScheduledTaskFailedWithName": "{0} αποτυχία", "ScheduledTaskStartedWithName": "{0} ξεκίνησε", "ServerNameNeedsToBeRestarted": "{0} χρειάζεται επανεκκίνηση", @@ -79,7 +79,7 @@ "System": "Σύστημα", "TvShows": "Τηλεοπτικές Σειρές", "User": "Χρήστης", - "UserCreatedWithName": "Δημιουργήθηκε ο χρήστης {0}", + "UserCreatedWithName": "Ο χρήστης {0} δημιουργήθηκε", "UserDeletedWithName": "Ο χρήστης {0} έχει διαγραφεί", "UserDownloadingItemWithValues": "{0} κατεβάζει {1}", "UserLockedOutWithName": "Ο χρήστης {0} αποκλείστηκε", @@ -93,29 +93,29 @@ "ValueSpecialEpisodeName": "Σπέσιαλ - {0}", "VersionNumber": "Έκδοση {0}", "TaskRefreshPeople": "Ανανέωση Ατόμων", - "TaskCleanLogsDescription": "Διαγράφει τα αρχεία καταγραφής που είναι άνω των {0} ημερών.", - "TaskCleanLogs": "Καθαρισμός Καταλόγου Καταγραφής", - "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και αναζωογονεί τα μεταδεδομένα.", + "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.", + "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής", + "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.", "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων", - "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο με κεφάλαια.", + "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.", "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου", - "TaskCleanCacheDescription": "Τα διαγραμμένα αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον από το σύστημα.", + "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.", "TaskCleanCache": "Καθαρισμός Καταλόγου Προσωρινής Μνήμης", "TasksChannelsCategory": "Κανάλια Διαδικτύου", "TasksApplicationCategory": "Εφαρμογή", "TasksLibraryCategory": "Βιβλιοθήκη", "TasksMaintenanceCategory": "Συντήρηση", - "TaskDownloadMissingSubtitlesDescription": "Αναζητήσεις στο διαδίκτυο όπου λείπουν υπότιτλους με βάση τη διαμόρφωση μεταδεδομένων.", + "TaskDownloadMissingSubtitlesDescription": "Ψάχνει στο διαδίκτυο για υπότιτλους που λείπουν με βάση τη διαμόρφωση μεταδεδομένων.", "TaskDownloadMissingSubtitles": "Λήψη υπότιτλων που λείπουν", "TaskRefreshChannelsDescription": "Ανανεώνει τις πληροφορίες καναλιού στο διαδικτύου.", "TaskRefreshChannels": "Ανανέωση Καναλιών", - "TaskCleanTranscodeDescription": "Διαγράφει αρχείου διακωδικοποιητή περισσότερο από μία ημέρα.", - "TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή", - "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.", - "TaskUpdatePlugins": "Ενημέρωση Προσθηκών", - "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.", + "TaskCleanTranscodeDescription": "Διαγράφει αρχεία διακωδικοποίησης άνω της μίας ημέρας.", + "TaskCleanTranscode": "Εκκαθάριση Kαταλόγου Διακωδικοποίησης", + "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τα πρόσθετα που έχουν ρυθμιστεί για αυτόματη ενημέρωση.", + "TaskUpdatePlugins": "Ενημέρωση Πρόσθετων", + "TaskRefreshPeopleDescription": "Ενημερώνει τα μεταδεδομένα για ηθοποιούς και σκηνοθέτες στη βιβλιοθήκη πολυμέσων σας.", "TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής παλαιότερες από την επιλεγμένη ηλικία.", - "TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων", + "TaskCleanActivityLog": "Εκκαθάριση Αρχείου Καταγραφής Δραστηριοτήτων", "Undefined": "Απροσδιόριστο", "Forced": "Εξαναγκασμένο", "Default": "Προεπιλογή", From 51c7c85b4cdaed9e86fc8699de61e489c9223a17 Mon Sep 17 00:00:00 2001 From: Stephen Cox Date: Fri, 6 Jan 2023 21:54:37 +0000 Subject: [PATCH 43/97] Translated using Weblate (Afrikaans) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/af/ --- Emby.Server.Implementations/Localization/Core/af.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index f356c98a93..9fbf364efe 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "Optimaliseer databasis", "TaskKeyframeExtractorDescription": "Haal keyframes vanuit video lêers om meer presiese HLS afspeellyste te maak. Dit kan lank duur.", "TaskKeyframeExtractor": "Keyframe Ekstraktor", - "External": "Ekstern" + "External": "Ekstern", + "HearingImpaired": "gehoorgestremd" } From 8af854315ef2ad78d37c25fd3def4efe6392a3ca Mon Sep 17 00:00:00 2001 From: MBR-0001 <55142207+MBR-0001@users.noreply.github.com> Date: Mon, 9 Jan 2023 20:47:12 +0100 Subject: [PATCH 44/97] Add Chinese Bilingual language (#7623) Closes https://github.com/jellyfin/jellyfin-plugin-opensubtitles/issues/103 --- Emby.Server.Implementations/Localization/iso6392.txt | 1 + .../Localization/LocalizationManagerTests.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index 66fba33304..b55c0fa330 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -77,6 +77,7 @@ chb|||Chibcha|chibcha che||ce|Chechen|tchétchène chg|||Chagatai|djaghataï chi|zho|zh|Chinese|chinois +chi|zho|ze|Chinese; Bilingual|chinois chi|zho|zh-tw|Chinese; Traditional|chinois chi|zho|zh-hk|Chinese; Hong Kong|chinois chk|||Chuukese|chuuk diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 3e7d6ed1dc..16eb7a75c6 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var cultures = localizationManager.GetCultures().ToList(); - Assert.Equal(190, cultures.Count); + Assert.Equal(191, cultures.Count); var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal)); Assert.NotNull(germany); From 6806b983dc41ef1cdf4e1d97d0ae4129f4a18cb2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 10:55:54 +0000 Subject: [PATCH 45/97] chore(deps): update dependency efcoresecondlevelcacheinterceptor to v3.8.2 --- .../Jellyfin.Server.Implementations.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index e982906737..68e9205e40 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -26,7 +26,7 @@ - + From b5da0d1b1775bbbf7acb29a5ebebca7ccd9e8f2e Mon Sep 17 00:00:00 2001 From: Patrick Barron <18354464+barronpm@users.noreply.github.com> Date: Tue, 10 Jan 2023 07:51:46 -0500 Subject: [PATCH 46/97] Rename Emby.Drawing and move to src (#9054) * Move Emby.Drawing to src * Rename Emby.Drawing -> Jellyfin.Drawing --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- .../Emby.Server.Implementations.csproj | 2 +- Jellyfin.Server/CoreAppHost.cs | 2 +- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- Jellyfin.sln | 3 ++- {Emby.Drawing => src/Jellyfin.Drawing}/ImageProcessor.cs | 2 +- .../Jellyfin.Drawing/Jellyfin.Drawing.csproj | 8 ++++---- .../Jellyfin.Drawing}/NullImageEncoder.cs | 2 +- .../Jellyfin.Drawing}/Properties/AssemblyInfo.cs | 2 +- 9 files changed, 13 insertions(+), 12 deletions(-) rename {Emby.Drawing => src/Jellyfin.Drawing}/ImageProcessor.cs (99%) rename Emby.Drawing/Emby.Drawing.csproj => src/Jellyfin.Drawing/Jellyfin.Drawing.csproj (78%) rename {Emby.Drawing => src/Jellyfin.Drawing}/NullImageEncoder.cs (98%) rename {Emby.Drawing => src/Jellyfin.Drawing}/Properties/AssemblyInfo.cs (96%) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5db3748bf7..7b3d07dfc1 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -18,7 +18,6 @@ using System.Threading.Tasks; using Emby.Dlna; using Emby.Dlna.Main; using Emby.Dlna.Ssdp; -using Emby.Drawing; using Emby.Naming.Common; using Emby.Notifications; using Emby.Photos; @@ -45,6 +44,7 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; +using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index f46affc73b..b18a3174a0 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -18,7 +18,7 @@ - + diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 002193baf5..d70b8f3ab7 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Reflection; -using Emby.Drawing; using Emby.Server.Implementations; using Emby.Server.Implementations.Session; using Jellyfin.Api.WebSocketListeners; +using Jellyfin.Drawing; using Jellyfin.Drawing.Skia; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index ac20869353..2be628ac22 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -52,7 +52,7 @@ - + diff --git a/Jellyfin.sln b/Jellyfin.sln index 0514b9614c..347716eb9a 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -17,7 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.LocalMetadata", "MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj", "{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Drawing", "Emby.Drawing\Emby.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing", "src\Jellyfin.Drawing\Jellyfin.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}" EndProject @@ -287,6 +287,7 @@ Global {DA9FD356-4894-4830-B208-D6BCE3E65B11} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {FE47334C-EFDE-4519-BD50-F24430FF360B} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/Emby.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs similarity index 99% rename from Emby.Drawing/ImageProcessor.cs rename to src/Jellyfin.Drawing/ImageProcessor.cs index 5a49e876a9..3c7bc394f0 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -19,7 +19,7 @@ using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; using Photo = MediaBrowser.Controller.Entities.Photo; -namespace Emby.Drawing +namespace Jellyfin.Drawing { /// /// Class ImageProcessor. diff --git a/Emby.Drawing/Emby.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj similarity index 78% rename from Emby.Drawing/Emby.Drawing.csproj rename to src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index 5bf226408b..a5bc8eaa7e 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -12,13 +12,13 @@ - - - + + + - + diff --git a/Emby.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs similarity index 98% rename from Emby.Drawing/NullImageEncoder.cs rename to src/Jellyfin.Drawing/NullImageEncoder.cs index d0a26b713b..24dda108ec 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/src/Jellyfin.Drawing/NullImageEncoder.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Drawing; -namespace Emby.Drawing +namespace Jellyfin.Drawing { /// /// A fallback implementation of . diff --git a/Emby.Drawing/Properties/AssemblyInfo.cs b/src/Jellyfin.Drawing/Properties/AssemblyInfo.cs similarity index 96% rename from Emby.Drawing/Properties/AssemblyInfo.cs rename to src/Jellyfin.Drawing/Properties/AssemblyInfo.cs index 281008e370..3851bf9241 100644 --- a/Emby.Drawing/Properties/AssemblyInfo.cs +++ b/src/Jellyfin.Drawing/Properties/AssemblyInfo.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("Emby.Drawing")] +[assembly: AssemblyTitle("Jellyfin.Drawing")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] From 407c716f82da25fa5ffe30a819ea1adb5c6873ab Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 2 Jan 2023 22:26:54 +0100 Subject: [PATCH 47/97] Add stereo downmix algorithm selection. --- .../Controllers/DynamicHlsController.cs | 8 +- .../MediaEncoding/EncodingHelper.cs | 28 +- .../Configuration/EncodingOptions.cs | 363 ++++++++++++------ .../Entities/DownMixStereoAlgorithms.cs | 23 ++ 4 files changed, 292 insertions(+), 130 deletions(-) create mode 100644 MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index af43bb578e..e0c5bcc84b 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; @@ -1731,7 +1732,12 @@ namespace Jellyfin.Api.Controllers var channels = state.OutputAudioChannels; - if (channels.HasValue) + if (channels.HasValue + && (channels.Value != 2 + || (state.AudioStream is not null + && state.AudioStream.Channels.HasValue + && state.AudioStream.Channels.Value > 5 + && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))) { args += " -ac " + channels.Value; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b40c224d5d..e94a04a7dc 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2129,15 +2129,30 @@ namespace MediaBrowser.Controller.MediaEncoding var filters = new List(); - // Boost volume to 200% when downsampling from 6ch to 2ch if (channels.HasValue - && channels.Value <= 2 + && channels.Value == 2 && state.AudioStream is not null && state.AudioStream.Channels.HasValue - && state.AudioStream.Channels.Value > 5 - && !encodingOptions.DownMixAudioBoost.Equals(1)) + && state.AudioStream.Channels.Value > 5) { - filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); + switch (encodingOptions.DownMixStereoAlgorithm) + { + case DownMixStereoAlgorithms.Dave750: + filters.Add("volume=4.25"); + filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3"); + break; + case DownMixStereoAlgorithms.NightmodeDialogue: + filters.Add("pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5"); + break; + case DownMixStereoAlgorithms.None: + default: + if (!encodingOptions.DownMixAudioBoost.Equals(1)) + { + filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); + } + + break; + } } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; @@ -5711,10 +5726,9 @@ namespace MediaBrowser.Controller.MediaEncoding return args; } - // Add the number of audio channels var channels = state.OutputAudioChannels; - if (channels.HasValue) + if (channels.HasValue && ((channels.Value != 2 && state.AudioStream.Channels <= 5) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)) { args += " -ac " + channels.Value; } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index f4cd2f0065..0ff95a2e1f 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -1,128 +1,247 @@ #nullable disable -#pragma warning disable CS1591 +using MediaBrowser.Model.Entities; -namespace MediaBrowser.Model.Configuration +namespace MediaBrowser.Model.Configuration; + +/// +/// Class EncodingOptions. +/// +public class EncodingOptions { - public class EncodingOptions + /// + /// Initializes a new instance of the class. + /// + public EncodingOptions() { - public EncodingOptions() - { - EnableFallbackFont = false; - DownMixAudioBoost = 2; - MaxMuxingQueueSize = 2048; - EnableThrottling = false; - ThrottleDelaySeconds = 180; - EncodingThreadCount = -1; - // This is a DRM device that is almost guaranteed to be there on every intel platform, - // plus it's the default one in ffmpeg if you don't specify anything - VaapiDevice = "/dev/dri/renderD128"; - EnableTonemapping = false; - EnableVppTonemapping = false; - TonemappingAlgorithm = "bt2390"; - TonemappingRange = "auto"; - TonemappingDesat = 0; - TonemappingThreshold = 0.8; - TonemappingPeak = 100; - TonemappingParam = 0; - VppTonemappingBrightness = 0; - VppTonemappingContrast = 1.2; - H264Crf = 23; - H265Crf = 28; - DeinterlaceDoubleRate = false; - DeinterlaceMethod = "yadif"; - EnableDecodingColorDepth10Hevc = true; - EnableDecodingColorDepth10Vp9 = true; - EnableEnhancedNvdecDecoder = false; - PreferSystemNativeHwDecoder = true; - EnableIntelLowPowerH264HwEncoder = false; - EnableIntelLowPowerHevcHwEncoder = false; - EnableHardwareEncoding = true; - AllowHevcEncoding = false; - EnableSubtitleExtraction = true; - AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; - HardwareDecodingCodecs = new string[] { "h264", "vc1" }; - } - - public int EncodingThreadCount { get; set; } - - public string TranscodingTempPath { get; set; } - - public string FallbackFontPath { get; set; } - - public bool EnableFallbackFont { get; set; } - - public double DownMixAudioBoost { get; set; } - - public int MaxMuxingQueueSize { get; set; } - - public bool EnableThrottling { get; set; } - - public int ThrottleDelaySeconds { get; set; } - - public string HardwareAccelerationType { get; set; } - - /// - /// Gets or sets the FFmpeg path as set by the user via the UI. - /// - public string EncoderAppPath { get; set; } - - /// - /// Gets or sets the current FFmpeg path being used by the system and displayed on the transcode page. - /// - public string EncoderAppPathDisplay { get; set; } - - public string VaapiDevice { get; set; } - - public bool EnableTonemapping { get; set; } - - public bool EnableVppTonemapping { get; set; } - - public string TonemappingAlgorithm { get; set; } - - public string TonemappingRange { get; set; } - - public double TonemappingDesat { get; set; } - - public double TonemappingThreshold { get; set; } - - public double TonemappingPeak { get; set; } - - public double TonemappingParam { get; set; } - - public double VppTonemappingBrightness { get; set; } - - public double VppTonemappingContrast { get; set; } - - public int H264Crf { get; set; } - - public int H265Crf { get; set; } - - public string EncoderPreset { get; set; } - - public bool DeinterlaceDoubleRate { get; set; } - - public string DeinterlaceMethod { get; set; } - - public bool EnableDecodingColorDepth10Hevc { get; set; } - - public bool EnableDecodingColorDepth10Vp9 { get; set; } - - public bool EnableEnhancedNvdecDecoder { get; set; } - - public bool PreferSystemNativeHwDecoder { get; set; } - - public bool EnableIntelLowPowerH264HwEncoder { get; set; } - - public bool EnableIntelLowPowerHevcHwEncoder { get; set; } - - public bool EnableHardwareEncoding { get; set; } - - public bool AllowHevcEncoding { get; set; } - - public bool EnableSubtitleExtraction { get; set; } - - public string[] HardwareDecodingCodecs { get; set; } - - public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; } + EnableFallbackFont = false; + DownMixAudioBoost = 2; + DownMixStereoAlgorithm = DownMixStereoAlgorithms.None; + MaxMuxingQueueSize = 2048; + EnableThrottling = false; + ThrottleDelaySeconds = 180; + EncodingThreadCount = -1; + // This is a DRM device that is almost guaranteed to be there on every intel platform, + // plus it's the default one in ffmpeg if you don't specify anything + VaapiDevice = "/dev/dri/renderD128"; + EnableTonemapping = false; + EnableVppTonemapping = false; + TonemappingAlgorithm = "bt2390"; + TonemappingRange = "auto"; + TonemappingDesat = 0; + TonemappingThreshold = 0.8; + TonemappingPeak = 100; + TonemappingParam = 0; + VppTonemappingBrightness = 0; + VppTonemappingContrast = 1.2; + H264Crf = 23; + H265Crf = 28; + DeinterlaceDoubleRate = false; + DeinterlaceMethod = "yadif"; + EnableDecodingColorDepth10Hevc = true; + EnableDecodingColorDepth10Vp9 = true; + EnableEnhancedNvdecDecoder = false; + PreferSystemNativeHwDecoder = true; + EnableIntelLowPowerH264HwEncoder = false; + EnableIntelLowPowerHevcHwEncoder = false; + EnableHardwareEncoding = true; + AllowHevcEncoding = false; + EnableSubtitleExtraction = true; + AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; + HardwareDecodingCodecs = new string[] { "h264", "vc1" }; } + + /// + /// Gets or sets the thread count used for encoding. + /// + public int EncodingThreadCount { get; set; } + + /// + /// Gets or sets the temporary transcoding path. + /// + public string TranscodingTempPath { get; set; } + + /// + /// Gets or sets the path to the fallback font. + /// + public string FallbackFontPath { get; set; } + + /// + /// Gets or sets a value indicating whether to use the fallback font. + /// + public bool EnableFallbackFont { get; set; } + + /// + /// Gets or sets the audio boost applied when downmixing audio. + /// + public double DownMixAudioBoost { get; set; } + + /// + /// Gets or sets the algorithm used for downmixing audio to stereo. + /// + public DownMixStereoAlgorithms DownMixStereoAlgorithm { get; set; } + + /// + /// Gets or sets the maximum size of the muxing queue. + /// + public int MaxMuxingQueueSize { get; set; } + + /// + /// Gets or sets a value indicating whether throttling is enabled. + /// + public bool EnableThrottling { get; set; } + + /// + /// Gets or sets the delay after which throttling happens. + /// + public int ThrottleDelaySeconds { get; set; } + + /// + /// Gets or sets the hardware acceleration type. + /// + public string HardwareAccelerationType { get; set; } + + /// + /// Gets or sets the FFmpeg path as set by the user via the UI. + /// + public string EncoderAppPath { get; set; } + + /// + /// Gets or sets the current FFmpeg path being used by the system and displayed on the transcode page. + /// + public string EncoderAppPathDisplay { get; set; } + + /// + /// Gets or sets the VA-API device. + /// + public string VaapiDevice { get; set; } + + /// + /// Gets or sets a value indicating whether tonemapping is enabled. + /// + public bool EnableTonemapping { get; set; } + + /// + /// Gets or sets a value indicating whether VPP tonemapping is enabled. + /// + public bool EnableVppTonemapping { get; set; } + + /// + /// Gets or sets the tone-mapping algorithm. + /// + public string TonemappingAlgorithm { get; set; } + + /// + /// Gets or sets the tone-mapping range. + /// + public string TonemappingRange { get; set; } + + /// + /// Gets or sets the tone-mapping desaturation. + /// + public double TonemappingDesat { get; set; } + + /// + /// Gets or sets the tone-mapping threshold. + /// + public double TonemappingThreshold { get; set; } + + /// + /// Gets or sets the tone-mapping peak. + /// + public double TonemappingPeak { get; set; } + + /// + /// Gets or sets the tone-mapping parameters. + /// + public double TonemappingParam { get; set; } + + /// + /// Gets or sets the VPP tone-mapping brightness. + /// + public double VppTonemappingBrightness { get; set; } + + /// + /// Gets or sets the VPP tone-mapping contrast. + /// + public double VppTonemappingContrast { get; set; } + + /// + /// Gets or sets the H264 CRF. + /// + public int H264Crf { get; set; } + + /// + /// Gets or sets the H265 CRF. + /// + public int H265Crf { get; set; } + + /// + /// Gets or sets the encoder preset. + /// + public string EncoderPreset { get; set; } + + /// + /// Gets or sets a value indicating whether the framerate is doubled when deinterlacing. + /// + public bool DeinterlaceDoubleRate { get; set; } + + /// + /// Gets or sets the deinterlace method. + /// + public string DeinterlaceMethod { get; set; } + + /// + /// Gets or sets a value indicating whether 10bit HEVC decoding is enabled. + /// + public bool EnableDecodingColorDepth10Hevc { get; set; } + + /// + /// Gets or sets a value indicating whether 10bit VP9 decoding is enabled. + /// + public bool EnableDecodingColorDepth10Vp9 { get; set; } + + /// + /// Gets or sets a value indicating whether the enhanced NVDEC is enabled. + /// + public bool EnableEnhancedNvdecDecoder { get; set; } + + /// + /// Gets or sets a value indicating whether the system native hardware decoder should be used. + /// + public bool PreferSystemNativeHwDecoder { get; set; } + + /// + /// Gets or sets a value indicating whether the Intel H264 low-power hardware encoder should be used. + /// + public bool EnableIntelLowPowerH264HwEncoder { get; set; } + + /// + /// Gets or sets a value indicating whether the Intel HEVC low-power hardware encoder should be used. + /// + public bool EnableIntelLowPowerHevcHwEncoder { get; set; } + + /// + /// Gets or sets a value indicating whether hardware encoding is enabled. + /// + public bool EnableHardwareEncoding { get; set; } + + /// + /// Gets or sets a value indicating whether HEVC encoding is enabled. + /// + public bool AllowHevcEncoding { get; set; } + + /// + /// Gets or sets a value indicating whether subtitle extraction is enabled. + /// + public bool EnableSubtitleExtraction { get; set; } + + /// + /// Gets or sets the codecs hardware encoding is used for. + /// + public string[] HardwareDecodingCodecs { get; set; } + + /// + /// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for. + /// + public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; } } diff --git a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs new file mode 100644 index 0000000000..385cd6a34e --- /dev/null +++ b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Model.Entities; + +/// +/// An enum representing an algorithm to downmix 6ch+ to stereo. +/// Algorithms sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620. +/// +public enum DownMixStereoAlgorithms +{ + /// + /// No special algorithm. + /// + None = 0, + + /// + /// Algorithm by Dave_750. + /// + Dave750 = 1, + + /// + /// Nightmode Dialogue algorithm. + /// + NightmodeDialogue = 2 +} From 7516e61c5b1992a2488f73ca481b86fa079c04fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 14:50:35 +0000 Subject: [PATCH 48/97] chore(deps): update dotnet monorepo to v7.0.2 --- .../Emby.Server.Implementations.csproj | 2 +- Jellyfin.Api/Jellyfin.Api.csproj | 2 +- .../Jellyfin.Server.Implementations.csproj | 8 ++++---- Jellyfin.Server/Jellyfin.Server.csproj | 4 ++-- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b18a3174a0..7accc3b8ba 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,7 +29,7 @@ - + diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index eca68c6dfa..b5444138fb 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -13,7 +13,7 @@ - + diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 68e9205e40..c2c3513ad9 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -28,13 +28,13 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 2be628ac22..1cd65ea825 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -37,8 +37,8 @@ - - + + diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 4a66edb16f..6434621c46 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -19,7 +19,7 @@ - + From 627c2b83efa8a2ecf1335d35bb7939d11ebc1d35 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Tue, 10 Jan 2023 10:53:28 -0700 Subject: [PATCH 49/97] Update docker sdk to 7.0.2 --- deployment/Dockerfile.centos.amd64 | 2 +- deployment/Dockerfile.fedora.amd64 | 2 +- deployment/Dockerfile.ubuntu.amd64 | 2 +- deployment/Dockerfile.ubuntu.arm64 | 2 +- deployment/Dockerfile.ubuntu.armhf | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index f7b7e30253..e02087a525 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 666937e5ca..6962b6bc18 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 0ad0132ccf..96e3ca403b 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 4f7ac20999..f1c5363999 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index af439e6eb7..eaea305d1e 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet From 2045eb109b664d39ee1516eba04fba1970225b8c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 14:56:55 -0700 Subject: [PATCH 50/97] chore(deps): update dependency sqlitepclraw.bundle_e_sqlite3 to v2.1.4 (#9062) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 1cd65ea825..bfe110323c 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -48,7 +48,7 @@ - + From 3ed0e70eaba16da29b212acc0d4f4ee50f15405b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 14:57:17 -0700 Subject: [PATCH 51/97] chore(deps): update dependency tmdblib to v2 (#9053) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- MediaBrowser.Providers/MediaBrowser.Providers.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index dbacc2a821..9713e02297 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -24,7 +24,7 @@ - + From 16e33665a217cb6b37d88cca244eb1538d41b873 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 10 Jan 2023 19:35:06 -0500 Subject: [PATCH 52/97] Move Jellyfin.Drawing.Skia to src --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- Jellyfin.sln | 3 ++- .../Jellyfin.Drawing.Skia}/Jellyfin.Drawing.Skia.csproj | 8 ++++---- .../Jellyfin.Drawing.Skia}/PercentPlayedDrawer.cs | 0 .../Jellyfin.Drawing.Skia}/PlayedIndicatorDrawer.cs | 0 .../Jellyfin.Drawing.Skia}/Properties/AssemblyInfo.cs | 0 .../Jellyfin.Drawing.Skia}/SkiaCodecException.cs | 0 .../Jellyfin.Drawing.Skia}/SkiaEncoder.cs | 0 .../Jellyfin.Drawing.Skia}/SkiaException.cs | 0 .../Jellyfin.Drawing.Skia}/SkiaHelper.cs | 0 .../Jellyfin.Drawing.Skia}/SplashscreenBuilder.cs | 0 .../Jellyfin.Drawing.Skia}/StripCollageBuilder.cs | 0 .../Jellyfin.Drawing.Skia}/UnplayedCountIndicator.cs | 0 13 files changed, 7 insertions(+), 6 deletions(-) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/Jellyfin.Drawing.Skia.csproj (82%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/PercentPlayedDrawer.cs (100%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/PlayedIndicatorDrawer.cs (100%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/Properties/AssemblyInfo.cs (100%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/SkiaCodecException.cs (100%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/SkiaEncoder.cs (100%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/SkiaException.cs (100%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/SkiaHelper.cs (100%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/SplashscreenBuilder.cs (100%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/StripCollageBuilder.cs (100%) rename {Jellyfin.Drawing.Skia => src/Jellyfin.Drawing.Skia}/UnplayedCountIndicator.cs (100%) diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 1cd65ea825..bff886cf50 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -54,7 +54,7 @@ - + diff --git a/Jellyfin.sln b/Jellyfin.sln index 347716eb9a..c0d2ec0687 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -42,7 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution SharedVersion.cs = SharedVersion.cs EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "src\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api", "Jellyfin.Api\Jellyfin.Api.csproj", "{DFBEFB4C-DA19-4143-98B7-27320C7F7163}" EndProject @@ -288,6 +288,7 @@ Global {FE47334C-EFDE-4519-BD50-F24430FF360B} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} + {154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj similarity index 82% rename from Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj rename to src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index dac3d0a61a..c686b229a6 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -12,7 +12,7 @@ - + @@ -24,9 +24,9 @@ - - - + + + diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs similarity index 100% rename from Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs rename to src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs similarity index 100% rename from Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs rename to src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs diff --git a/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs b/src/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs similarity index 100% rename from Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs rename to src/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs similarity index 100% rename from Jellyfin.Drawing.Skia/SkiaCodecException.cs rename to src/Jellyfin.Drawing.Skia/SkiaCodecException.cs diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs similarity index 100% rename from Jellyfin.Drawing.Skia/SkiaEncoder.cs rename to src/Jellyfin.Drawing.Skia/SkiaEncoder.cs diff --git a/Jellyfin.Drawing.Skia/SkiaException.cs b/src/Jellyfin.Drawing.Skia/SkiaException.cs similarity index 100% rename from Jellyfin.Drawing.Skia/SkiaException.cs rename to src/Jellyfin.Drawing.Skia/SkiaException.cs diff --git a/Jellyfin.Drawing.Skia/SkiaHelper.cs b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs similarity index 100% rename from Jellyfin.Drawing.Skia/SkiaHelper.cs rename to src/Jellyfin.Drawing.Skia/SkiaHelper.cs diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs similarity index 100% rename from Jellyfin.Drawing.Skia/SplashscreenBuilder.cs rename to src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs similarity index 100% rename from Jellyfin.Drawing.Skia/StripCollageBuilder.cs rename to src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs similarity index 100% rename from Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs rename to src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs From 6c7225b94357ad9b75103cd4a689cf6e6c338bd1 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 10 Jan 2023 19:38:57 -0500 Subject: [PATCH 53/97] Use file-scoped namespaces in Jellyfin.Drawing --- src/Jellyfin.Drawing/ImageProcessor.cs | 941 +++++++++++------------ src/Jellyfin.Drawing/NullImageEncoder.cs | 81 +- 2 files changed, 510 insertions(+), 512 deletions(-) diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 3c7bc394f0..b381c9ae73 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -19,551 +19,550 @@ using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; using Photo = MediaBrowser.Controller.Entities.Photo; -namespace Jellyfin.Drawing +namespace Jellyfin.Drawing; + +/// +/// Class ImageProcessor. +/// +public sealed class ImageProcessor : IImageProcessor, IDisposable { + // Increment this when there's a change requiring caches to be invalidated + private const char Version = '3'; + + private static readonly HashSet _transparentImageTypes + = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; + + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationPaths _appPaths; + private readonly IImageEncoder _imageEncoder; + private readonly IMediaEncoder _mediaEncoder; + + private bool _disposed; + /// - /// Class ImageProcessor. + /// Initializes a new instance of the class. /// - public sealed class ImageProcessor : IImageProcessor, IDisposable + /// The logger. + /// The server application paths. + /// The filesystem. + /// The image encoder. + /// The media encoder. + public ImageProcessor( + ILogger logger, + IServerApplicationPaths appPaths, + IFileSystem fileSystem, + IImageEncoder imageEncoder, + IMediaEncoder mediaEncoder) { - // Increment this when there's a change requiring caches to be invalidated - private const char Version = '3'; + _logger = logger; + _fileSystem = fileSystem; + _imageEncoder = imageEncoder; + _mediaEncoder = mediaEncoder; + _appPaths = appPaths; + } - private static readonly HashSet _transparentImageTypes - = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; + private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IServerApplicationPaths _appPaths; - private readonly IImageEncoder _imageEncoder; - private readonly IMediaEncoder _mediaEncoder; - - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The server application paths. - /// The filesystem. - /// The image encoder. - /// The media encoder. - public ImageProcessor( - ILogger logger, - IServerApplicationPaths appPaths, - IFileSystem fileSystem, - IImageEncoder imageEncoder, - IMediaEncoder mediaEncoder) + /// + public IReadOnlyCollection SupportedInputFormats => + new HashSet(StringComparer.OrdinalIgnoreCase) { - _logger = logger; - _fileSystem = fileSystem; - _imageEncoder = imageEncoder; - _mediaEncoder = mediaEncoder; - _appPaths = appPaths; + "tiff", + "tif", + "jpeg", + "jpg", + "png", + "aiff", + "cr2", + "crw", + "nef", + "orf", + "pef", + "arw", + "webp", + "gif", + "bmp", + "erf", + "raf", + "rw2", + "nrw", + "dng", + "ico", + "astc", + "ktx", + "pkm", + "wbmp" + }; + + /// + public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; + + /// + public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) + { + var file = await ProcessImage(options).ConfigureAwait(false); + using (var fileStream = AsyncFile.OpenRead(file.Path)) + { + await fileStream.CopyToAsync(toStream).ConfigureAwait(false); + } + } + + /// + public IReadOnlyCollection GetSupportedImageOutputFormats() + => _imageEncoder.SupportedOutputFormats; + + /// + public bool SupportsTransparency(string path) + => _transparentImageTypes.Contains(Path.GetExtension(path)); + + /// + public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options) + { + ItemImageInfo originalImage = options.Image; + BaseItem item = options.Item; + + string originalImagePath = originalImage.Path; + DateTime dateModified = originalImage.DateModified; + ImageDimensions? originalImageSize = null; + if (originalImage.Width > 0 && originalImage.Height > 0) + { + originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height); } - private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); - - /// - public IReadOnlyCollection SupportedInputFormats => - new HashSet(StringComparer.OrdinalIgnoreCase) - { - "tiff", - "tif", - "jpeg", - "jpg", - "png", - "aiff", - "cr2", - "crw", - "nef", - "orf", - "pef", - "arw", - "webp", - "gif", - "bmp", - "erf", - "raf", - "rw2", - "nrw", - "dng", - "ico", - "astc", - "ktx", - "pkm", - "wbmp" - }; - - /// - public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; - - /// - public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) + var mimeType = MimeTypes.GetMimeType(originalImagePath); + if (!_imageEncoder.SupportsImageEncoding) { - var file = await ProcessImage(options).ConfigureAwait(false); - using (var fileStream = AsyncFile.OpenRead(file.Path)) - { - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - } + return (originalImagePath, mimeType, dateModified); } - /// - public IReadOnlyCollection GetSupportedImageOutputFormats() - => _imageEncoder.SupportedOutputFormats; + var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false); + originalImagePath = supportedImageInfo.Path; - /// - public bool SupportsTransparency(string path) - => _transparentImageTypes.Contains(Path.GetExtension(path)); - - /// - public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options) + // Original file doesn't exist, or original file is gif. + if (!File.Exists(originalImagePath) || string.Equals(mimeType, MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase)) { - ItemImageInfo originalImage = options.Image; - BaseItem item = options.Item; + return (originalImagePath, mimeType, dateModified); + } - string originalImagePath = originalImage.Path; - DateTime dateModified = originalImage.DateModified; - ImageDimensions? originalImageSize = null; - if (originalImage.Width > 0 && originalImage.Height > 0) + dateModified = supportedImageInfo.DateModified; + bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath)); + + bool autoOrient = false; + ImageOrientation? orientation = null; + if (item is Photo photo) + { + if (photo.Orientation.HasValue) { - originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height); - } - - var mimeType = MimeTypes.GetMimeType(originalImagePath); - if (!_imageEncoder.SupportsImageEncoding) - { - return (originalImagePath, mimeType, dateModified); - } - - var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false); - originalImagePath = supportedImageInfo.Path; - - // Original file doesn't exist, or original file is gif. - if (!File.Exists(originalImagePath) || string.Equals(mimeType, MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase)) - { - return (originalImagePath, mimeType, dateModified); - } - - dateModified = supportedImageInfo.DateModified; - bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath)); - - bool autoOrient = false; - ImageOrientation? orientation = null; - if (item is Photo photo) - { - if (photo.Orientation.HasValue) + if (photo.Orientation.Value != ImageOrientation.TopLeft) { - if (photo.Orientation.Value != ImageOrientation.TopLeft) - { - autoOrient = true; - orientation = photo.Orientation; - } - } - else - { - // Orientation unknown, so do it autoOrient = true; orientation = photo.Orientation; } } - - if (options.HasDefaultOptions(originalImagePath, originalImageSize) && (!autoOrient || !options.RequiresAutoOrientation)) + else { - // Just spit out the original file if all the options are default - return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); - } - - int quality = options.Quality; - - ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); - string cacheFilePath = GetCacheFilePath( - originalImagePath, - options.Width, - options.Height, - options.MaxWidth, - options.MaxHeight, - options.FillWidth, - options.FillHeight, - quality, - dateModified, - outputFormat, - options.AddPlayedIndicator, - options.PercentPlayed, - options.UnplayedCount, - options.Blur, - options.BackgroundColor, - options.ForegroundLayer); - - try - { - if (!File.Exists(cacheFilePath)) - { - string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat); - - if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase)) - { - return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); - } - } - - return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); - } - catch (Exception ex) - { - // If it fails for whatever reason, return the original image - _logger.LogError(ex, "Error encoding image"); - return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); + // Orientation unknown, so do it + autoOrient = true; + orientation = photo.Orientation; } } - private ImageFormat GetOutputFormat(IReadOnlyCollection clientSupportedFormats, bool requiresTransparency) + if (options.HasDefaultOptions(originalImagePath, originalImageSize) && (!autoOrient || !options.RequiresAutoOrientation)) { - var serverFormats = GetSupportedImageOutputFormats(); + // Just spit out the original file if all the options are default + return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); + } - // Client doesn't care about format, so start with webp if supported - if (serverFormats.Contains(ImageFormat.Webp) && clientSupportedFormats.Contains(ImageFormat.Webp)) - { - return ImageFormat.Webp; - } + int quality = options.Quality; - // If transparency is needed and webp isn't supported, than png is the only option - if (requiresTransparency && clientSupportedFormats.Contains(ImageFormat.Png)) - { - return ImageFormat.Png; - } + ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); + string cacheFilePath = GetCacheFilePath( + originalImagePath, + options.Width, + options.Height, + options.MaxWidth, + options.MaxHeight, + options.FillWidth, + options.FillHeight, + quality, + dateModified, + outputFormat, + options.AddPlayedIndicator, + options.PercentPlayed, + options.UnplayedCount, + options.Blur, + options.BackgroundColor, + options.ForegroundLayer); - foreach (var format in clientSupportedFormats) + try + { + if (!File.Exists(cacheFilePath)) { - if (serverFormats.Contains(format)) + string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat); + + if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase)) { - return format; + return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } } - // We should never actually get here - return ImageFormat.Jpg; + return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); + } + catch (Exception ex) + { + // If it fails for whatever reason, return the original image + _logger.LogError(ex, "Error encoding image"); + return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); + } + } + + private ImageFormat GetOutputFormat(IReadOnlyCollection clientSupportedFormats, bool requiresTransparency) + { + var serverFormats = GetSupportedImageOutputFormats(); + + // Client doesn't care about format, so start with webp if supported + if (serverFormats.Contains(ImageFormat.Webp) && clientSupportedFormats.Contains(ImageFormat.Webp)) + { + return ImageFormat.Webp; } - private string GetMimeType(ImageFormat format, string path) - => format switch - { - ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"), - ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"), - ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"), - ImageFormat.Png => MimeTypes.GetMimeType("i.png"), - ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"), - _ => MimeTypes.GetMimeType(path) - }; - - /// - /// Gets the cache file path based on a set of parameters. - /// - private string GetCacheFilePath( - string originalPath, - int? width, - int? height, - int? maxWidth, - int? maxHeight, - int? fillWidth, - int? fillHeight, - int quality, - DateTime dateModified, - ImageFormat format, - bool addPlayedIndicator, - double percentPlayed, - int? unwatchedCount, - int? blur, - string backgroundColor, - string foregroundLayer) + // If transparency is needed and webp isn't supported, than png is the only option + if (requiresTransparency && clientSupportedFormats.Contains(ImageFormat.Png)) { - var filename = new StringBuilder(256); - filename.Append(originalPath); - - filename.Append(",quality="); - filename.Append(quality); - - filename.Append(",datemodified="); - filename.Append(dateModified.Ticks); - - filename.Append(",f="); - filename.Append(format); - - if (width.HasValue) - { - filename.Append(",width="); - filename.Append(width.Value); - } - - if (height.HasValue) - { - filename.Append(",height="); - filename.Append(height.Value); - } - - if (maxWidth.HasValue) - { - filename.Append(",maxwidth="); - filename.Append(maxWidth.Value); - } - - if (maxHeight.HasValue) - { - filename.Append(",maxheight="); - filename.Append(maxHeight.Value); - } - - if (fillWidth.HasValue) - { - filename.Append(",fillwidth="); - filename.Append(fillWidth.Value); - } - - if (fillHeight.HasValue) - { - filename.Append(",fillheight="); - filename.Append(fillHeight.Value); - } - - if (addPlayedIndicator) - { - filename.Append(",pl=true"); - } - - if (percentPlayed > 0) - { - filename.Append(",p="); - filename.Append(percentPlayed); - } - - if (unwatchedCount.HasValue) - { - filename.Append(",p="); - filename.Append(unwatchedCount.Value); - } - - if (blur.HasValue) - { - filename.Append(",blur="); - filename.Append(blur.Value); - } - - if (!string.IsNullOrEmpty(backgroundColor)) - { - filename.Append(",b="); - filename.Append(backgroundColor); - } - - if (!string.IsNullOrEmpty(foregroundLayer)) - { - filename.Append(",fl="); - filename.Append(foregroundLayer); - } - - filename.Append(",v="); - filename.Append(Version); - - return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); + return ImageFormat.Png; } - /// - public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info) + foreach (var format in clientSupportedFormats) { - int width = info.Width; - int height = info.Height; - - if (height > 0 && width > 0) + if (serverFormats.Contains(format)) { - return new ImageDimensions(width, height); + return format; } - - string path = info.Path; - _logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path); - - ImageDimensions size = GetImageDimensions(path); - info.Width = size.Width; - info.Height = size.Height; - - return size; } - /// - public ImageDimensions GetImageDimensions(string path) - => _imageEncoder.GetImageSize(path); + // We should never actually get here + return ImageFormat.Jpg; + } - /// - public string GetImageBlurHash(string path) + private string GetMimeType(ImageFormat format, string path) + => format switch { - var size = GetImageDimensions(path); - return GetImageBlurHash(path, size); + ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"), + ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"), + ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"), + ImageFormat.Png => MimeTypes.GetMimeType("i.png"), + ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"), + _ => MimeTypes.GetMimeType(path) + }; + + /// + /// Gets the cache file path based on a set of parameters. + /// + private string GetCacheFilePath( + string originalPath, + int? width, + int? height, + int? maxWidth, + int? maxHeight, + int? fillWidth, + int? fillHeight, + int quality, + DateTime dateModified, + ImageFormat format, + bool addPlayedIndicator, + double percentPlayed, + int? unwatchedCount, + int? blur, + string backgroundColor, + string foregroundLayer) + { + var filename = new StringBuilder(256); + filename.Append(originalPath); + + filename.Append(",quality="); + filename.Append(quality); + + filename.Append(",datemodified="); + filename.Append(dateModified.Ticks); + + filename.Append(",f="); + filename.Append(format); + + if (width.HasValue) + { + filename.Append(",width="); + filename.Append(width.Value); } - /// - public string GetImageBlurHash(string path, ImageDimensions imageDimensions) + if (height.HasValue) { - if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0) - { - return string.Empty; - } - - // We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance. - // One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width. - // See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components - float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height); - float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width; - - int xComp = Math.Min((int)xCompF + 1, 9); - int yComp = Math.Min((int)yCompF + 1, 9); - - return _imageEncoder.GetImageBlurHash(xComp, yComp, path); + filename.Append(",height="); + filename.Append(height.Value); } - /// - public string GetImageCacheTag(BaseItem item, ItemImageInfo image) - => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); - - /// - public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) + if (maxWidth.HasValue) { - return GetImageCacheTag(item, new ItemImageInfo - { - Path = chapter.ImagePath, - Type = ImageType.Chapter, - DateModified = chapter.ImageDateModified - }); + filename.Append(",maxwidth="); + filename.Append(maxWidth.Value); } - /// - public string? GetImageCacheTag(User user) + if (maxHeight.HasValue) { - if (user.ProfileImage is null) - { - return null; - } - - return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5() - .ToString("N", CultureInfo.InvariantCulture); + filename.Append(",maxheight="); + filename.Append(maxHeight.Value); } - private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) + if (fillWidth.HasValue) { - var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString(); + filename.Append(",fillwidth="); + filename.Append(fillWidth.Value); + } - // These are just jpg files renamed as tbn - if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase)) - { - return Task.FromResult((originalImagePath, dateModified)); - } + if (fillHeight.HasValue) + { + filename.Append(",fillheight="); + filename.Append(fillHeight.Value); + } - // TODO _mediaEncoder.ConvertImage is not implemented - // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) - // { - // try - // { - // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); - // - // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; - // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); - // - // var file = _fileSystem.GetFileInfo(outputPath); - // if (!file.Exists) - // { - // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); - // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); - // } - // else - // { - // dateModified = file.LastWriteTimeUtc; - // } - // - // originalImagePath = outputPath; - // } - // catch (Exception ex) - // { - // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath); - // } - // } + if (addPlayedIndicator) + { + filename.Append(",pl=true"); + } + if (percentPlayed > 0) + { + filename.Append(",p="); + filename.Append(percentPlayed); + } + + if (unwatchedCount.HasValue) + { + filename.Append(",p="); + filename.Append(unwatchedCount.Value); + } + + if (blur.HasValue) + { + filename.Append(",blur="); + filename.Append(blur.Value); + } + + if (!string.IsNullOrEmpty(backgroundColor)) + { + filename.Append(",b="); + filename.Append(backgroundColor); + } + + if (!string.IsNullOrEmpty(foregroundLayer)) + { + filename.Append(",fl="); + filename.Append(foregroundLayer); + } + + filename.Append(",v="); + filename.Append(Version); + + return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); + } + + /// + public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info) + { + int width = info.Width; + int height = info.Height; + + if (height > 0 && width > 0) + { + return new ImageDimensions(width, height); + } + + string path = info.Path; + _logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path); + + ImageDimensions size = GetImageDimensions(path); + info.Width = size.Width; + info.Height = size.Height; + + return size; + } + + /// + public ImageDimensions GetImageDimensions(string path) + => _imageEncoder.GetImageSize(path); + + /// + public string GetImageBlurHash(string path) + { + var size = GetImageDimensions(path); + return GetImageBlurHash(path, size); + } + + /// + public string GetImageBlurHash(string path, ImageDimensions imageDimensions) + { + if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0) + { + return string.Empty; + } + + // We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance. + // One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width. + // See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components + float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height); + float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width; + + int xComp = Math.Min((int)xCompF + 1, 9); + int yComp = Math.Min((int)yCompF + 1, 9); + + return _imageEncoder.GetImageBlurHash(xComp, yComp, path); + } + + /// + public string GetImageCacheTag(BaseItem item, ItemImageInfo image) + => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + + /// + public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) + { + return GetImageCacheTag(item, new ItemImageInfo + { + Path = chapter.ImagePath, + Type = ImageType.Chapter, + DateModified = chapter.ImageDateModified + }); + } + + /// + public string? GetImageCacheTag(User user) + { + if (user.ProfileImage is null) + { + return null; + } + + return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5() + .ToString("N", CultureInfo.InvariantCulture); + } + + private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) + { + var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString(); + + // These are just jpg files renamed as tbn + if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase)) + { return Task.FromResult((originalImagePath, dateModified)); } - /// - /// Gets the cache path. - /// - /// The path. - /// Name of the unique. - /// The file extension. - /// System.String. - /// - /// path - /// or - /// uniqueName - /// or - /// fileExtension. - /// - public string GetCachePath(string path, string uniqueName, string fileExtension) + // TODO _mediaEncoder.ConvertImage is not implemented + // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) + // { + // try + // { + // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); + // + // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; + // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); + // + // var file = _fileSystem.GetFileInfo(outputPath); + // if (!file.Exists) + // { + // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); + // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); + // } + // else + // { + // dateModified = file.LastWriteTimeUtc; + // } + // + // originalImagePath = outputPath; + // } + // catch (Exception ex) + // { + // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath); + // } + // } + + return Task.FromResult((originalImagePath, dateModified)); + } + + /// + /// Gets the cache path. + /// + /// The path. + /// Name of the unique. + /// The file extension. + /// System.String. + /// + /// path + /// or + /// uniqueName + /// or + /// fileExtension. + /// + public string GetCachePath(string path, string uniqueName, string fileExtension) + { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentException.ThrowIfNullOrEmpty(uniqueName); + ArgumentException.ThrowIfNullOrEmpty(fileExtension); + + var filename = uniqueName.GetMD5() + fileExtension; + + return GetCachePath(path, filename); + } + + /// + /// Gets the cache path. + /// + /// The path. + /// The filename. + /// System.String. + /// + /// path + /// or + /// filename. + /// + public string GetCachePath(ReadOnlySpan path, ReadOnlySpan filename) + { + if (path.IsEmpty) { - ArgumentException.ThrowIfNullOrEmpty(path); - ArgumentException.ThrowIfNullOrEmpty(uniqueName); - ArgumentException.ThrowIfNullOrEmpty(fileExtension); - - var filename = uniqueName.GetMD5() + fileExtension; - - return GetCachePath(path, filename); + throw new ArgumentException("Path can't be empty.", nameof(path)); } - /// - /// Gets the cache path. - /// - /// The path. - /// The filename. - /// System.String. - /// - /// path - /// or - /// filename. - /// - public string GetCachePath(ReadOnlySpan path, ReadOnlySpan filename) + if (filename.IsEmpty) { - if (path.IsEmpty) - { - throw new ArgumentException("Path can't be empty.", nameof(path)); - } - - if (filename.IsEmpty) - { - throw new ArgumentException("Filename can't be empty.", nameof(filename)); - } - - var prefix = filename.Slice(0, 1); - - return Path.Join(path, prefix, filename); + throw new ArgumentException("Filename can't be empty.", nameof(filename)); } - /// - public void CreateImageCollage(ImageCollageOptions options, string? libraryName) + var prefix = filename.Slice(0, 1); + + return Path.Join(path, prefix, filename); + } + + /// + public void CreateImageCollage(ImageCollageOptions options, string? libraryName) + { + _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath); + + _imageEncoder.CreateImageCollage(options, libraryName); + + _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath); + } + + /// + public void Dispose() + { + if (_disposed) { - _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath); - - _imageEncoder.CreateImageCollage(options, libraryName); - - _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath); + return; } - /// - public void Dispose() + if (_imageEncoder is IDisposable disposable) { - if (_disposed) - { - return; - } - - if (_imageEncoder is IDisposable disposable) - { - disposable.Dispose(); - } - - _disposed = true; + disposable.Dispose(); } + + _disposed = true; } } diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs index 24dda108ec..171128bed3 100644 --- a/src/Jellyfin.Drawing/NullImageEncoder.cs +++ b/src/Jellyfin.Drawing/NullImageEncoder.cs @@ -3,56 +3,55 @@ using System.Collections.Generic; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Drawing; -namespace Jellyfin.Drawing -{ - /// - /// A fallback implementation of . - /// - public class NullImageEncoder : IImageEncoder - { - /// - public IReadOnlyCollection SupportedInputFormats - => new HashSet(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" }; +namespace Jellyfin.Drawing; - /// - public IReadOnlyCollection SupportedOutputFormats +/// +/// A fallback implementation of . +/// +public class NullImageEncoder : IImageEncoder +{ + /// + public IReadOnlyCollection SupportedInputFormats + => new HashSet(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" }; + + /// + public IReadOnlyCollection SupportedOutputFormats => new HashSet() { ImageFormat.Jpg, ImageFormat.Png }; - /// - public string Name => "Null Image Encoder"; + /// + public string Name => "Null Image Encoder"; - /// - public bool SupportsImageCollageCreation => false; + /// + public bool SupportsImageCollageCreation => false; - /// - public bool SupportsImageEncoding => false; + /// + public bool SupportsImageEncoding => false; - /// - public ImageDimensions GetImageSize(string path) - => throw new NotImplementedException(); + /// + public ImageDimensions GetImageSize(string path) + => throw new NotImplementedException(); - /// - public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat) - { - throw new NotImplementedException(); - } + /// + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat) + { + throw new NotImplementedException(); + } - /// - public void CreateImageCollage(ImageCollageOptions options, string? libraryName) - { - throw new NotImplementedException(); - } + /// + public void CreateImageCollage(ImageCollageOptions options, string? libraryName) + { + throw new NotImplementedException(); + } - /// - public void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops) - { - throw new NotImplementedException(); - } + /// + public void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops) + { + throw new NotImplementedException(); + } - /// - public string GetImageBlurHash(int xComp, int yComp, string path) - { - throw new NotImplementedException(); - } + /// + public string GetImageBlurHash(int xComp, int yComp, string path) + { + throw new NotImplementedException(); } } From cafc454cfbd316175ce1234741699ce43cb43341 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 10 Jan 2023 19:41:55 -0500 Subject: [PATCH 54/97] Use file-scoped namespaces in Jellyfin.Drawing.Skia --- .../PercentPlayedDrawer.cs | 45 +- .../PlayedIndicatorDrawer.cs | 63 +- .../SkiaCodecException.cs | 71 +- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 895 +++++++++--------- src/Jellyfin.Drawing.Skia/SkiaException.cs | 57 +- src/Jellyfin.Drawing.Skia/SkiaHelper.cs | 65 +- .../SplashscreenBuilder.cs | 235 +++-- .../StripCollageBuilder.cs | 301 +++--- .../UnplayedCountIndicator.cs | 93 +- 9 files changed, 908 insertions(+), 917 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs index 6136a2ff98..e2e90be475 100644 --- a/src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs +++ b/src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs @@ -2,35 +2,34 @@ using System; using MediaBrowser.Model.Drawing; using SkiaSharp; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// Static helper class used to draw percentage-played indicators on images. +/// +public static class PercentPlayedDrawer { + private const int IndicatorHeight = 8; + /// - /// Static helper class used to draw percentage-played indicators on images. + /// Draw a percentage played indicator on a canvas. /// - public static class PercentPlayedDrawer + /// The canvas to draw the indicator on. + /// The size of the image being drawn on. + /// The percentage played to display with the indicator. + public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent) { - private const int IndicatorHeight = 8; + using var paint = new SKPaint(); + var endX = imageSize.Width - 1; + var endY = imageSize.Height - 1; - /// - /// Draw a percentage played indicator on a canvas. - /// - /// The canvas to draw the indicator on. - /// The size of the image being drawn on. - /// The percentage played to display with the indicator. - public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent) - { - using var paint = new SKPaint(); - var endX = imageSize.Width - 1; - var endY = imageSize.Height - 1; + paint.Color = SKColor.Parse("#99000000"); + paint.Style = SKPaintStyle.Fill; + canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint); - paint.Color = SKColor.Parse("#99000000"); - paint.Style = SKPaintStyle.Fill; - canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint); + double foregroundWidth = (endX * percent) / 100; - double foregroundWidth = (endX * percent) / 100; - - paint.Color = SKColor.Parse("#FF00A4DC"); - canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint); - } + paint.Color = SKColor.Parse("#FF00A4DC"); + canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint); } } diff --git a/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs index 2a37299428..5bb42fb99e 100644 --- a/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs +++ b/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs @@ -1,48 +1,47 @@ using MediaBrowser.Model.Drawing; using SkiaSharp; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// Static helper class for drawing 'played' indicators. +/// +public static class PlayedIndicatorDrawer { + private const int OffsetFromTopRightCorner = 38; + /// - /// Static helper class for drawing 'played' indicators. + /// Draw a 'played' indicator in the top right corner of a canvas. /// - public static class PlayedIndicatorDrawer + /// The canvas to draw the indicator on. + /// + /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the + /// indicator. + /// + public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize) { - private const int OffsetFromTopRightCorner = 38; + var x = imageSize.Width - OffsetFromTopRightCorner; - /// - /// Draw a 'played' indicator in the top right corner of a canvas. - /// - /// The canvas to draw the indicator on. - /// - /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the - /// indicator. - /// - public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize) + using var paint = new SKPaint { - var x = imageSize.Width - OffsetFromTopRightCorner; + Color = SKColor.Parse("#CC00A4DC"), + Style = SKPaintStyle.Fill + }; - using var paint = new SKPaint - { - Color = SKColor.Parse("#CC00A4DC"), - Style = SKPaintStyle.Fill - }; + canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); - canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); + paint.Color = new SKColor(255, 255, 255, 255); + paint.TextSize = 30; + paint.IsAntialias = true; - paint.Color = new SKColor(255, 255, 255, 255); - paint.TextSize = 30; - paint.IsAntialias = true; + // or: + // var emojiChar = 0x1F680; + const string Text = "✔️"; + var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32); - // or: - // var emojiChar = 0x1F680; - const string Text = "✔️"; - var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32); + // ask the font manager for a font with that character + paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar); - // ask the font manager for a font with that character - paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar); - - canvas.DrawText(Text, (float)x - 12, OffsetFromTopRightCorner + 12, paint); - } + canvas.DrawText(Text, (float)x - 12, OffsetFromTopRightCorner + 12, paint); } } diff --git a/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs index 9a50a4d62e..581fa000dc 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs @@ -1,45 +1,44 @@ using System.Globalization; using SkiaSharp; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// Represents errors that occur during interaction with Skia codecs. +/// +public class SkiaCodecException : SkiaException { /// - /// Represents errors that occur during interaction with Skia codecs. + /// Initializes a new instance of the class. /// - public class SkiaCodecException : SkiaException + /// The non-successful codec result returned by Skia. + public SkiaCodecException(SKCodecResult result) { - /// - /// Initializes a new instance of the class. - /// - /// The non-successful codec result returned by Skia. - public SkiaCodecException(SKCodecResult result) - { - CodecResult = result; - } - - /// - /// Initializes a new instance of the class - /// with a specified error message. - /// - /// The non-successful codec result returned by Skia. - /// The message that describes the error. - public SkiaCodecException(SKCodecResult result, string message) - : base(message) - { - CodecResult = result; - } - - /// - /// Gets the non-successful codec result returned by Skia. - /// - public SKCodecResult CodecResult { get; } - - /// - public override string ToString() - => string.Format( - CultureInfo.InvariantCulture, - "Non-success codec result: {0}\n{1}", - CodecResult, - base.ToString()); + CodecResult = result; } + + /// + /// Initializes a new instance of the class + /// with a specified error message. + /// + /// The non-successful codec result returned by Skia. + /// The message that describes the error. + public SkiaCodecException(SKCodecResult result, string message) + : base(message) + { + CodecResult = result; + } + + /// + /// Gets the non-successful codec result returned by Skia. + /// + public SKCodecResult CodecResult { get; } + + /// + public override string ToString() + => string.Format( + CultureInfo.InvariantCulture, + "Non-success codec result: {0}\n{1}", + CodecResult, + base.ToString()); } diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 9171c4d6e6..ddb8a98d4d 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -12,534 +12,533 @@ using Microsoft.Extensions.Logging; using SkiaSharp; using SKSvg = SkiaSharp.Extended.Svg.SKSvg; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// Image encoder that uses to manipulate images. +/// +public class SkiaEncoder : IImageEncoder { + private static readonly HashSet _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; + + private readonly ILogger _logger; + private readonly IApplicationPaths _appPaths; + /// - /// Image encoder that uses to manipulate images. + /// Initializes a new instance of the class. /// - public class SkiaEncoder : IImageEncoder + /// The application logger. + /// The application paths. + public SkiaEncoder(ILogger logger, IApplicationPaths appPaths) { - private static readonly HashSet _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; + _logger = logger; + _appPaths = appPaths; + } - private readonly ILogger _logger; - private readonly IApplicationPaths _appPaths; + /// + public string Name => "Skia"; - /// - /// Initializes a new instance of the class. - /// - /// The application logger. - /// The application paths. - public SkiaEncoder(ILogger logger, IApplicationPaths appPaths) + /// + public bool SupportsImageCollageCreation => true; + + /// + public bool SupportsImageEncoding => true; + + /// + public IReadOnlyCollection SupportedInputFormats => + new HashSet(StringComparer.OrdinalIgnoreCase) { - _logger = logger; - _appPaths = appPaths; + "jpeg", + "jpg", + "png", + "dng", + "webp", + "gif", + "bmp", + "ico", + "astc", + "ktx", + "pkm", + "wbmp", + // TODO: check if these are supported on multiple platforms + // https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454 + // working on windows at least + "cr2", + "nef", + "arw" + }; + + /// + public IReadOnlyCollection SupportedOutputFormats + => new HashSet { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; + + /// + /// Check if the native lib is available. + /// + /// True if the native lib is available, otherwise false. + public static bool IsNativeLibAvailable() + { + try + { + // test an operation that requires the native library + SKPMColor.PreMultiply(SKColors.Black); + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Convert a to a . + /// + /// The format to convert. + /// The converted format. + public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat) + { + return selectedFormat switch + { + ImageFormat.Bmp => SKEncodedImageFormat.Bmp, + ImageFormat.Jpg => SKEncodedImageFormat.Jpeg, + ImageFormat.Gif => SKEncodedImageFormat.Gif, + ImageFormat.Webp => SKEncodedImageFormat.Webp, + _ => SKEncodedImageFormat.Png + }; + } + + /// + /// The path is not valid. + public ImageDimensions GetImageSize(string path) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException("File not found", path); } - /// - public string Name => "Skia"; - - /// - public bool SupportsImageCollageCreation => true; - - /// - public bool SupportsImageEncoding => true; - - /// - public IReadOnlyCollection SupportedInputFormats => - new HashSet(StringComparer.OrdinalIgnoreCase) - { - "jpeg", - "jpg", - "png", - "dng", - "webp", - "gif", - "bmp", - "ico", - "astc", - "ktx", - "pkm", - "wbmp", - // TODO: check if these are supported on multiple platforms - // https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454 - // working on windows at least - "cr2", - "nef", - "arw" - }; - - /// - public IReadOnlyCollection SupportedOutputFormats - => new HashSet { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; - - /// - /// Check if the native lib is available. - /// - /// True if the native lib is available, otherwise false. - public static bool IsNativeLibAvailable() + var extension = Path.GetExtension(path.AsSpan()); + if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase)) { - try + var svg = new SKSvg(); + svg.Load(path); + return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height)); + } + + using var codec = SKCodec.Create(path, out SKCodecResult result); + switch (result) + { + case SKCodecResult.Success: + var info = codec.Info; + return new ImageDimensions(info.Width, info.Height); + case SKCodecResult.Unimplemented: + _logger.LogDebug("Image format not supported: {FilePath}", path); + return new ImageDimensions(0, 0); + default: + _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result); + return new ImageDimensions(0, 0); + } + } + + /// + /// The path is null. + /// The path is not valid. + /// The file at the specified path could not be used to generate a codec. + public string GetImageBlurHash(int xComp, int yComp, string path) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + var extension = Path.GetExtension(path.AsSpan()).TrimStart('.'); + if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path); + return string.Empty; + } + + // Any larger than 128x128 is too slow and there's no visually discernible difference + return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128); + } + + private bool RequiresSpecialCharacterHack(string path) + { + for (int i = 0; i < path.Length; i++) + { + if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter) { - // test an operation that requires the native library - SKPMColor.PreMultiply(SKColors.Black); return true; } - catch (Exception) - { - return false; - } } - /// - /// Convert a to a . - /// - /// The format to convert. - /// The converted format. - public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat) + return path.HasDiacritics(); + } + + private string NormalizePath(string path) + { + if (!RequiresSpecialCharacterHack(path)) { - return selectedFormat switch - { - ImageFormat.Bmp => SKEncodedImageFormat.Bmp, - ImageFormat.Jpg => SKEncodedImageFormat.Jpeg, - ImageFormat.Gif => SKEncodedImageFormat.Gif, - ImageFormat.Webp => SKEncodedImageFormat.Webp, - _ => SKEncodedImageFormat.Png - }; + return path; } - /// - /// The path is not valid. - public ImageDimensions GetImageSize(string path) + var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path)); + var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid."); + Directory.CreateDirectory(directory); + File.Copy(path, tempPath, true); + + return tempPath; + } + + private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation) + { + if (!orientation.HasValue) { - if (!File.Exists(path)) - { - throw new FileNotFoundException("File not found", path); - } - - var extension = Path.GetExtension(path.AsSpan()); - if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase)) - { - var svg = new SKSvg(); - svg.Load(path); - return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height)); - } - - using var codec = SKCodec.Create(path, out SKCodecResult result); - switch (result) - { - case SKCodecResult.Success: - var info = codec.Info; - return new ImageDimensions(info.Width, info.Height); - case SKCodecResult.Unimplemented: - _logger.LogDebug("Image format not supported: {FilePath}", path); - return new ImageDimensions(0, 0); - default: - _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result); - return new ImageDimensions(0, 0); - } + return SKEncodedOrigin.TopLeft; } - /// - /// The path is null. - /// The path is not valid. - /// The file at the specified path could not be used to generate a codec. - public string GetImageBlurHash(int xComp, int yComp, string path) + return orientation.Value switch { - ArgumentException.ThrowIfNullOrEmpty(path); + ImageOrientation.TopRight => SKEncodedOrigin.TopRight, + ImageOrientation.RightTop => SKEncodedOrigin.RightTop, + ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom, + ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop, + ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom, + ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight, + ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft, + _ => SKEncodedOrigin.TopLeft + }; + } - var extension = Path.GetExtension(path.AsSpan()).TrimStart('.'); - if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) - { - _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path); - return string.Empty; - } - - // Any larger than 128x128 is too slow and there's no visually discernible difference - return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128); + /// + /// Decode an image. + /// + /// The filepath of the image to decode. + /// Whether to force clean the bitmap. + /// The orientation of the image. + /// The detected origin of the image. + /// The resulting bitmap of the image. + internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException("File not found", path); } - private bool RequiresSpecialCharacterHack(string path) + var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path)); + + if (requiresTransparencyHack || forceCleanBitmap) { - for (int i = 0; i < path.Length; i++) + using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res); + if (res != SKCodecResult.Success) { - if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter) - { - return true; - } + origin = GetSKEncodedOrigin(orientation); + return null; } - return path.HasDiacritics(); + // create the bitmap + var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack); + + // decode + _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels()); + + origin = codec.EncodedOrigin; + + return bitmap; } - private string NormalizePath(string path) + var resultBitmap = SKBitmap.Decode(NormalizePath(path)); + + if (resultBitmap is null) { - if (!RequiresSpecialCharacterHack(path)) - { - return path; - } - - var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path)); - var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid."); - Directory.CreateDirectory(directory); - File.Copy(path, tempPath, true); - - return tempPath; + return Decode(path, true, orientation, out origin); } - private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation) + // If we have to resize these they often end up distorted + if (resultBitmap.ColorType == SKColorType.Gray8) { - if (!orientation.HasValue) - { - return SKEncodedOrigin.TopLeft; - } - - return orientation.Value switch - { - ImageOrientation.TopRight => SKEncodedOrigin.TopRight, - ImageOrientation.RightTop => SKEncodedOrigin.RightTop, - ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom, - ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop, - ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom, - ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight, - ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft, - _ => SKEncodedOrigin.TopLeft - }; - } - - /// - /// Decode an image. - /// - /// The filepath of the image to decode. - /// Whether to force clean the bitmap. - /// The orientation of the image. - /// The detected origin of the image. - /// The resulting bitmap of the image. - internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) - { - if (!File.Exists(path)) - { - throw new FileNotFoundException("File not found", path); - } - - var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path)); - - if (requiresTransparencyHack || forceCleanBitmap) - { - using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res); - if (res != SKCodecResult.Success) - { - origin = GetSKEncodedOrigin(orientation); - return null; - } - - // create the bitmap - var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack); - - // decode - _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels()); - - origin = codec.EncodedOrigin; - - return bitmap; - } - - var resultBitmap = SKBitmap.Decode(NormalizePath(path)); - - if (resultBitmap is null) + using (resultBitmap) { return Decode(path, true, orientation, out origin); } + } - // If we have to resize these they often end up distorted - if (resultBitmap.ColorType == SKColorType.Gray8) + origin = SKEncodedOrigin.TopLeft; + return resultBitmap; + } + + private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation) + { + if (autoOrient) + { + var bitmap = Decode(path, true, orientation, out var origin); + + if (bitmap is not null && origin != SKEncodedOrigin.TopLeft) { - using (resultBitmap) + using (bitmap) { - return Decode(path, true, orientation, out origin); + return OrientImage(bitmap, origin); } } - origin = SKEncodedOrigin.TopLeft; - return resultBitmap; + return bitmap; } - private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation) + return Decode(path, false, orientation, out _); + } + + private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) + { + var needsFlip = origin == SKEncodedOrigin.LeftBottom + || origin == SKEncodedOrigin.LeftTop + || origin == SKEncodedOrigin.RightBottom + || origin == SKEncodedOrigin.RightTop; + var rotated = needsFlip + ? new SKBitmap(bitmap.Height, bitmap.Width) + : new SKBitmap(bitmap.Width, bitmap.Height); + using var surface = new SKCanvas(rotated); + var midX = (float)rotated.Width / 2; + var midY = (float)rotated.Height / 2; + + switch (origin) { - if (autoOrient) - { - var bitmap = Decode(path, true, orientation, out var origin); - - if (bitmap is not null && origin != SKEncodedOrigin.TopLeft) - { - using (bitmap) - { - return OrientImage(bitmap, origin); - } - } - - return bitmap; - } - - return Decode(path, false, orientation, out _); + case SKEncodedOrigin.TopRight: + surface.Scale(-1, 1, midX, midY); + break; + case SKEncodedOrigin.BottomRight: + surface.RotateDegrees(180, midX, midY); + break; + case SKEncodedOrigin.BottomLeft: + surface.Scale(1, -1, midX, midY); + break; + case SKEncodedOrigin.LeftTop: + surface.Translate(0, -rotated.Height); + surface.Scale(1, -1, midX, midY); + surface.RotateDegrees(-90); + break; + case SKEncodedOrigin.RightTop: + surface.Translate(rotated.Width, 0); + surface.RotateDegrees(90); + break; + case SKEncodedOrigin.RightBottom: + surface.Translate(rotated.Width, 0); + surface.Scale(1, -1, midX, midY); + surface.RotateDegrees(90); + break; + case SKEncodedOrigin.LeftBottom: + surface.Translate(0, rotated.Height); + surface.RotateDegrees(-90); + break; } - private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) + surface.DrawBitmap(bitmap, 0, 0); + return rotated; + } + + /// + /// Resizes an image on the CPU, by utilizing a surface and canvas. + /// + /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect. + /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html). + /// + /// The source bitmap. + /// This specifies the target size and other information required to create the surface. + /// This enables anti-aliasing on the SKPaint instance. + /// This enables dithering on the SKPaint instance. + /// The resized image. + internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false) + { + using var surface = SKSurface.Create(targetInfo); + using var canvas = surface.Canvas; + using var paint = new SKPaint { - var needsFlip = origin == SKEncodedOrigin.LeftBottom - || origin == SKEncodedOrigin.LeftTop - || origin == SKEncodedOrigin.RightBottom - || origin == SKEncodedOrigin.RightTop; - var rotated = needsFlip - ? new SKBitmap(bitmap.Height, bitmap.Width) - : new SKBitmap(bitmap.Width, bitmap.Height); - using var surface = new SKCanvas(rotated); - var midX = (float)rotated.Width / 2; - var midY = (float)rotated.Height / 2; + FilterQuality = SKFilterQuality.High, + IsAntialias = isAntialias, + IsDither = isDither + }; - switch (origin) - { - case SKEncodedOrigin.TopRight: - surface.Scale(-1, 1, midX, midY); - break; - case SKEncodedOrigin.BottomRight: - surface.RotateDegrees(180, midX, midY); - break; - case SKEncodedOrigin.BottomLeft: - surface.Scale(1, -1, midX, midY); - break; - case SKEncodedOrigin.LeftTop: - surface.Translate(0, -rotated.Height); - surface.Scale(1, -1, midX, midY); - surface.RotateDegrees(-90); - break; - case SKEncodedOrigin.RightTop: - surface.Translate(rotated.Width, 0); - surface.RotateDegrees(90); - break; - case SKEncodedOrigin.RightBottom: - surface.Translate(rotated.Width, 0); - surface.Scale(1, -1, midX, midY); - surface.RotateDegrees(90); - break; - case SKEncodedOrigin.LeftBottom: - surface.Translate(0, rotated.Height); - surface.RotateDegrees(-90); - break; - } + var kernel = new float[9] + { + 0, -.1f, 0, + -.1f, 1.4f, -.1f, + 0, -.1f, 0, + }; - surface.DrawBitmap(bitmap, 0, 0); - return rotated; + var kernelSize = new SKSizeI(3, 3); + var kernelOffset = new SKPointI(1, 1); + + paint.ImageFilter = SKImageFilter.CreateMatrixConvolution( + kernelSize, + kernel, + 1f, + 0f, + kernelOffset, + SKShaderTileMode.Clamp, + true); + + canvas.DrawBitmap( + source, + SKRect.Create(0, 0, source.Width, source.Height), + SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height), + paint); + + return surface.Snapshot(); + } + + /// + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat) + { + ArgumentException.ThrowIfNullOrEmpty(inputPath); + ArgumentException.ThrowIfNullOrEmpty(outputPath); + + var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.'); + if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath); + return inputPath; } - /// - /// Resizes an image on the CPU, by utilizing a surface and canvas. - /// - /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect. - /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html). - /// - /// The source bitmap. - /// This specifies the target size and other information required to create the surface. - /// This enables anti-aliasing on the SKPaint instance. - /// This enables dithering on the SKPaint instance. - /// The resized image. - internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false) + var skiaOutputFormat = GetImageFormat(outputFormat); + + var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor); + var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer); + var blur = options.Blur ?? 0; + var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0); + + using var bitmap = GetBitmap(inputPath, autoOrient, orientation); + if (bitmap is null) { - using var surface = SKSurface.Create(targetInfo); - using var canvas = surface.Canvas; - using var paint = new SKPaint - { - FilterQuality = SKFilterQuality.High, - IsAntialias = isAntialias, - IsDither = isDither - }; - - var kernel = new float[9] - { - 0, -.1f, 0, - -.1f, 1.4f, -.1f, - 0, -.1f, 0, - }; - - var kernelSize = new SKSizeI(3, 3); - var kernelOffset = new SKPointI(1, 1); - - paint.ImageFilter = SKImageFilter.CreateMatrixConvolution( - kernelSize, - kernel, - 1f, - 0f, - kernelOffset, - SKShaderTileMode.Clamp, - true); - - canvas.DrawBitmap( - source, - SKRect.Create(0, 0, source.Width, source.Height), - SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height), - paint); - - return surface.Snapshot(); + throw new InvalidDataException($"Skia unable to read image {inputPath}"); } - /// - public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat) + var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); + + if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient) { - ArgumentException.ThrowIfNullOrEmpty(inputPath); - ArgumentException.ThrowIfNullOrEmpty(outputPath); + // Just spit out the original file if all the options are default + return inputPath; + } - var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.'); - if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase)) - { - _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath); - return inputPath; - } + var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); - var skiaOutputFormat = GetImageFormat(outputFormat); + var width = newImageSize.Width; + var height = newImageSize.Height; - var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor); - var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer); - var blur = options.Blur ?? 0; - var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0); - - using var bitmap = GetBitmap(inputPath, autoOrient, orientation); - if (bitmap is null) - { - throw new InvalidDataException($"Skia unable to read image {inputPath}"); - } - - var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); - - if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient) - { - // Just spit out the original file if all the options are default - return inputPath; - } - - var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); - - var width = newImageSize.Width; - var height = newImageSize.Height; - - // scale image (the FromImage creates a copy) - var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace); - using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo)); - - // If all we're doing is resizing then we can stop now - if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) - { - var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); - Directory.CreateDirectory(outputDirectory); - using var outputStream = new SKFileWStream(outputPath); - using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()); - resizedBitmap.Encode(outputStream, skiaOutputFormat, quality); - return outputPath; - } - - // create bitmap to use for canvas drawing used to draw into bitmap - using var saveBitmap = new SKBitmap(width, height); - using var canvas = new SKCanvas(saveBitmap); - // set background color if present - if (hasBackgroundColor) - { - canvas.Clear(SKColor.Parse(options.BackgroundColor)); - } - - // Add blur if option is present - if (blur > 0) - { - // create image from resized bitmap to apply blur - using var paint = new SKPaint(); - using var filter = SKImageFilter.CreateBlur(blur, blur); - paint.ImageFilter = filter; - canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint); - } - else - { - // draw resized bitmap onto canvas - canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height)); - } - - // If foreground layer present then draw - if (hasForegroundColor) - { - if (!double.TryParse(options.ForegroundLayer, out double opacity)) - { - opacity = .4; - } - - canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver); - } - - if (hasIndicator) - { - DrawIndicator(canvas, width, height, options); - } - - var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); - Directory.CreateDirectory(directory); - using (var outputStream = new SKFileWStream(outputPath)) - { - using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels())) - { - pixmap.Encode(outputStream, skiaOutputFormat, quality); - } - } + // scale image (the FromImage creates a copy) + var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace); + using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo)); + // If all we're doing is resizing then we can stop now + if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) + { + var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); + Directory.CreateDirectory(outputDirectory); + using var outputStream = new SKFileWStream(outputPath); + using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()); + resizedBitmap.Encode(outputStream, skiaOutputFormat, quality); return outputPath; } - /// - public void CreateImageCollage(ImageCollageOptions options, string? libraryName) + // create bitmap to use for canvas drawing used to draw into bitmap + using var saveBitmap = new SKBitmap(width, height); + using var canvas = new SKCanvas(saveBitmap); + // set background color if present + if (hasBackgroundColor) { - double ratio = (double)options.Width / options.Height; + canvas.Clear(SKColor.Parse(options.BackgroundColor)); + } - if (ratio >= 1.4) + // Add blur if option is present + if (blur > 0) + { + // create image from resized bitmap to apply blur + using var paint = new SKPaint(); + using var filter = SKImageFilter.CreateBlur(blur, blur); + paint.ImageFilter = filter; + canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint); + } + else + { + // draw resized bitmap onto canvas + canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height)); + } + + // If foreground layer present then draw + if (hasForegroundColor) + { + if (!double.TryParse(options.ForegroundLayer, out double opacity)) { - new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName); + opacity = .4; } - else if (ratio >= .9) + + canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver); + } + + if (hasIndicator) + { + DrawIndicator(canvas, width, height, options); + } + + var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); + Directory.CreateDirectory(directory); + using (var outputStream = new SKFileWStream(outputPath)) + { + using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels())) { - new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); - } - else - { - // TODO: Create Poster collage capability - new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + pixmap.Encode(outputStream, skiaOutputFormat, quality); } } - /// - public void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops) + return outputPath; + } + + /// + public void CreateImageCollage(ImageCollageOptions options, string? libraryName) + { + double ratio = (double)options.Width / options.Height; + + if (ratio >= 1.4) { - var splashBuilder = new SplashscreenBuilder(this); - var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); - splashBuilder.GenerateSplash(posters, backdrops, outputPath); + new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName); } - - private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) + else if (ratio >= .9) { - try - { - var currentImageSize = new ImageDimensions(imageWidth, imageHeight); + new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + } + else + { + // TODO: Create Poster collage capability + new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + } + } - if (options.AddPlayedIndicator) - { - PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize); - } - else if (options.UnplayedCount.HasValue) - { - UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value); - } + /// + public void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops) + { + var splashBuilder = new SplashscreenBuilder(this); + var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); + splashBuilder.GenerateSplash(posters, backdrops, outputPath); + } - if (options.PercentPlayed > 0) - { - PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed); - } - } - catch (Exception ex) + private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) + { + try + { + var currentImageSize = new ImageDimensions(imageWidth, imageHeight); + + if (options.AddPlayedIndicator) { - _logger.LogError(ex, "Error drawing indicator overlay"); + PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize); } + else if (options.UnplayedCount.HasValue) + { + UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value); + } + + if (options.PercentPlayed > 0) + { + PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error drawing indicator overlay"); } } } diff --git a/src/Jellyfin.Drawing.Skia/SkiaException.cs b/src/Jellyfin.Drawing.Skia/SkiaException.cs index 5b272eac57..d0e69d42c8 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaException.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaException.cs @@ -1,39 +1,38 @@ using System; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// Represents errors that occur during interaction with Skia. +/// +public class SkiaException : Exception { /// - /// Represents errors that occur during interaction with Skia. + /// Initializes a new instance of the class. /// - public class SkiaException : Exception + public SkiaException() { - /// - /// Initializes a new instance of the class. - /// - public SkiaException() - { - } + } - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public SkiaException(string message) : base(message) - { - } + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public SkiaException(string message) : base(message) + { + } - /// - /// Initializes a new instance of the class with a specified error message and a - /// reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// - /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if - /// no inner exception is specified. - /// - public SkiaException(string message, Exception innerException) - : base(message, innerException) - { - } + /// + /// Initializes a new instance of the class with a specified error message and a + /// reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if + /// no inner exception is specified. + /// + public SkiaException(string message, Exception innerException) + : base(message, innerException) + { } } diff --git a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs index 23e92dcb2d..00d224da94 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs @@ -1,47 +1,46 @@ using System.Collections.Generic; using SkiaSharp; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// Class containing helper methods for working with SkiaSharp. +/// +public static class SkiaHelper { /// - /// Class containing helper methods for working with SkiaSharp. + /// Gets the next valid image as a bitmap. /// - public static class SkiaHelper + /// The current skia encoder. + /// The list of image paths. + /// The current checked index. + /// The new index. + /// A valid bitmap, or null if no bitmap exists after currentIndex. + public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList paths, int currentIndex, out int newIndex) { - /// - /// Gets the next valid image as a bitmap. - /// - /// The current skia encoder. - /// The list of image paths. - /// The current checked index. - /// The new index. - /// A valid bitmap, or null if no bitmap exists after currentIndex. - public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList paths, int currentIndex, out int newIndex) + var imagesTested = new Dictionary(); + SKBitmap? bitmap = null; + + while (imagesTested.Count < paths.Count) { - var imagesTested = new Dictionary(); - SKBitmap? bitmap = null; - - while (imagesTested.Count < paths.Count) + if (currentIndex >= paths.Count) { - if (currentIndex >= paths.Count) - { - currentIndex = 0; - } - - bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _); - - imagesTested[currentIndex] = 0; - - currentIndex++; - - if (bitmap is not null) - { - break; - } + currentIndex = 0; } - newIndex = currentIndex; - return bitmap; + bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _); + + imagesTested[currentIndex] = 0; + + currentIndex++; + + if (bitmap is not null) + { + break; + } } + + newIndex = currentIndex; + return bitmap; } } diff --git a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs index 7fbae33495..9905566230 100644 --- a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -2,147 +2,146 @@ using System; using System.Collections.Generic; using SkiaSharp; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// Used to build the splashscreen. +/// +public class SplashscreenBuilder { + private const int FinalWidth = 1920; + private const int FinalHeight = 1080; + // generated collage resolution should be higher than the final resolution + private const int WallWidth = FinalWidth * 3; + private const int WallHeight = FinalHeight * 2; + private const int Rows = 6; + private const int Spacing = 20; + + private readonly SkiaEncoder _skiaEncoder; + /// - /// Used to build the splashscreen. + /// Initializes a new instance of the class. /// - public class SplashscreenBuilder + /// The SkiaEncoder. + public SplashscreenBuilder(SkiaEncoder skiaEncoder) { - private const int FinalWidth = 1920; - private const int FinalHeight = 1080; - // generated collage resolution should be higher than the final resolution - private const int WallWidth = FinalWidth * 3; - private const int WallHeight = FinalHeight * 2; - private const int Rows = 6; - private const int Spacing = 20; + _skiaEncoder = skiaEncoder; + } - private readonly SkiaEncoder _skiaEncoder; + /// + /// Generate a splashscreen. + /// + /// The poster paths. + /// The landscape paths. + /// The output path. + public void GenerateSplash(IReadOnlyList posters, IReadOnlyList backdrops, string outputPath) + { + using var wall = GenerateCollage(posters, backdrops); + using var transformed = Transform3D(wall); - /// - /// Initializes a new instance of the class. - /// - /// The SkiaEncoder. - public SplashscreenBuilder(SkiaEncoder skiaEncoder) + using var outputStream = new SKFileWStream(outputPath); + using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels()); + pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90); + } + + /// + /// Generates a collage of posters and landscape pictures. + /// + /// The poster paths. + /// The landscape paths. + /// The created collage as a bitmap. + private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList backdrops) + { + var posterIndex = 0; + var backdropIndex = 0; + + var bitmap = new SKBitmap(WallWidth, WallHeight); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Black); + + int posterHeight = WallHeight / 6; + + for (int i = 0; i < Rows; i++) { - _skiaEncoder = skiaEncoder; - } + int imageCounter = Random.Shared.Next(0, 5); + int currentWidthPos = i * 75; + int currentHeight = i * (posterHeight + Spacing); - /// - /// Generate a splashscreen. - /// - /// The poster paths. - /// The landscape paths. - /// The output path. - public void GenerateSplash(IReadOnlyList posters, IReadOnlyList backdrops, string outputPath) - { - using var wall = GenerateCollage(posters, backdrops); - using var transformed = Transform3D(wall); - - using var outputStream = new SKFileWStream(outputPath); - using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels()); - pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90); - } - - /// - /// Generates a collage of posters and landscape pictures. - /// - /// The poster paths. - /// The landscape paths. - /// The created collage as a bitmap. - private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList backdrops) - { - var posterIndex = 0; - var backdropIndex = 0; - - var bitmap = new SKBitmap(WallWidth, WallHeight); - using var canvas = new SKCanvas(bitmap); - canvas.Clear(SKColors.Black); - - int posterHeight = WallHeight / 6; - - for (int i = 0; i < Rows; i++) + while (currentWidthPos < WallWidth) { - int imageCounter = Random.Shared.Next(0, 5); - int currentWidthPos = i * 75; - int currentHeight = i * (posterHeight + Spacing); + SKBitmap? currentImage; - while (currentWidthPos < WallWidth) + switch (imageCounter) { - SKBitmap? currentImage; + case 0: + case 2: + case 3: + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex); + posterIndex = newPosterIndex; + break; + default: + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex); + backdropIndex = newBackdropIndex; + break; + } - switch (imageCounter) - { - case 0: - case 2: - case 3: - currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex); - posterIndex = newPosterIndex; - break; - default: - currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex); - backdropIndex = newBackdropIndex; - break; - } + if (currentImage is null) + { + throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!"); + } - if (currentImage is null) - { - throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!"); - } + // resize to the same aspect as the original + var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height); + using var resizedBitmap = new SKBitmap(imageWidth, posterHeight); + currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High); - // resize to the same aspect as the original - var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height); - using var resizedBitmap = new SKBitmap(imageWidth, posterHeight); - currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High); + // draw on canvas + canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight); - // draw on canvas - canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight); + currentWidthPos += imageWidth + Spacing; - currentWidthPos += imageWidth + Spacing; + currentImage.Dispose(); - currentImage.Dispose(); - - if (imageCounter >= 4) - { - imageCounter = 0; - } - else - { - imageCounter++; - } + if (imageCounter >= 4) + { + imageCounter = 0; + } + else + { + imageCounter++; } } - - return bitmap; } - /// - /// Transform the collage in 3D space. - /// - /// The bitmap to transform. - /// The transformed image. - private SKBitmap Transform3D(SKBitmap input) + return bitmap; + } + + /// + /// Transform the collage in 3D space. + /// + /// The bitmap to transform. + /// The transformed image. + private SKBitmap Transform3D(SKBitmap input) + { + var bitmap = new SKBitmap(FinalWidth, FinalHeight); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Black); + var matrix = new SKMatrix { - var bitmap = new SKBitmap(FinalWidth, FinalHeight); - using var canvas = new SKCanvas(bitmap); - canvas.Clear(SKColors.Black); - var matrix = new SKMatrix - { - ScaleX = 0.324108899f, - ScaleY = 0.563934922f, - SkewX = -0.244337708f, - SkewY = 0.0377609022f, - TransX = 42.0407715f, - TransY = -198.104706f, - Persp0 = -9.08959337E-05f, - Persp1 = 6.85242048E-05f, - Persp2 = 0.988209724f - }; + ScaleX = 0.324108899f, + ScaleY = 0.563934922f, + SkewX = -0.244337708f, + SkewY = 0.0377609022f, + TransX = 42.0407715f, + TransY = -198.104706f, + Persp0 = -9.08959337E-05f, + Persp1 = 6.85242048E-05f, + Persp2 = 0.988209724f + }; - canvas.SetMatrix(matrix); - canvas.DrawBitmap(input, 0, 0); + canvas.SetMatrix(matrix); + canvas.DrawBitmap(input, 0, 0); - return bitmap; - } + return bitmap; } } diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index c8b8f3ace4..eee24c4236 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -4,183 +4,182 @@ using System.IO; using System.Text.RegularExpressions; using SkiaSharp; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// Used to build collages of multiple images arranged in vertical strips. +/// +public class StripCollageBuilder { + private readonly SkiaEncoder _skiaEncoder; + /// - /// Used to build collages of multiple images arranged in vertical strips. + /// Initializes a new instance of the class. /// - public class StripCollageBuilder + /// The encoder to use for building collages. + public StripCollageBuilder(SkiaEncoder skiaEncoder) { - private readonly SkiaEncoder _skiaEncoder; + _skiaEncoder = skiaEncoder; + } - /// - /// Initializes a new instance of the class. - /// - /// The encoder to use for building collages. - public StripCollageBuilder(SkiaEncoder skiaEncoder) + /// + /// Check which format an image has been encoded with using its filename extension. + /// + /// The path to the image to get the format for. + /// The image format. + public static SKEncodedImageFormat GetEncodedFormat(string outputPath) + { + ArgumentNullException.ThrowIfNull(outputPath); + + var ext = Path.GetExtension(outputPath); + + if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase) + || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase)) { - _skiaEncoder = skiaEncoder; + return SKEncodedImageFormat.Jpeg; } - /// - /// Check which format an image has been encoded with using its filename extension. - /// - /// The path to the image to get the format for. - /// The image format. - public static SKEncodedImageFormat GetEncodedFormat(string outputPath) + if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase)) { - ArgumentNullException.ThrowIfNull(outputPath); - - var ext = Path.GetExtension(outputPath); - - if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase) - || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase)) - { - return SKEncodedImageFormat.Jpeg; - } - - if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase)) - { - return SKEncodedImageFormat.Webp; - } - - if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase)) - { - return SKEncodedImageFormat.Gif; - } - - if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase)) - { - return SKEncodedImageFormat.Bmp; - } - - // default to png - return SKEncodedImageFormat.Png; + return SKEncodedImageFormat.Webp; } - /// - /// Create a square collage. - /// - /// The paths of the images to use in the collage. - /// The path at which to place the resulting collage image. - /// The desired width of the collage. - /// The desired height of the collage. - public void BuildSquareCollage(IReadOnlyList paths, string outputPath, int width, int height) + if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase)) { - using var bitmap = BuildSquareCollageBitmap(paths, width, height); - using var outputStream = new SKFileWStream(outputPath); - using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()); - pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); + return SKEncodedImageFormat.Gif; } - /// - /// Create a thumb collage. - /// - /// The paths of the images to use in the collage. - /// The path at which to place the resulting image. - /// The desired width of the collage. - /// The desired height of the collage. - /// The name of the library to draw on the collage. - public void BuildThumbCollage(IReadOnlyList paths, string outputPath, int width, int height, string? libraryName) + if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase)) { - using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName); - using var outputStream = new SKFileWStream(outputPath); - using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()); - pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); + return SKEncodedImageFormat.Bmp; } - private SKBitmap BuildThumbCollageBitmap(IReadOnlyList paths, int width, int height, string? libraryName) + // default to png + return SKEncodedImageFormat.Png; + } + + /// + /// Create a square collage. + /// + /// The paths of the images to use in the collage. + /// The path at which to place the resulting collage image. + /// The desired width of the collage. + /// The desired height of the collage. + public void BuildSquareCollage(IReadOnlyList paths, string outputPath, int width, int height) + { + using var bitmap = BuildSquareCollageBitmap(paths, width, height); + using var outputStream = new SKFileWStream(outputPath); + using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()); + pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); + } + + /// + /// Create a thumb collage. + /// + /// The paths of the images to use in the collage. + /// The path at which to place the resulting image. + /// The desired width of the collage. + /// The desired height of the collage. + /// The name of the library to draw on the collage. + public void BuildThumbCollage(IReadOnlyList paths, string outputPath, int width, int height, string? libraryName) + { + using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName); + using var outputStream = new SKFileWStream(outputPath); + using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()); + pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); + } + + private SKBitmap BuildThumbCollageBitmap(IReadOnlyList paths, int width, int height, string? libraryName) + { + var bitmap = new SKBitmap(width, height); + + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Black); + + using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _); + if (backdrop is null) { - var bitmap = new SKBitmap(width, height); - - using var canvas = new SKCanvas(bitmap); - canvas.Clear(SKColors.Black); - - using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _); - if (backdrop is null) - { - return bitmap; - } - - // resize to the same aspect as the original - var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width); - using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace)); - // draw the backdrop - canvas.DrawImage(residedBackdrop, 0, 0); - - // draw shadow rectangle - using var paintColor = new SKPaint - { - Color = SKColors.Black.WithAlpha(0x78), - Style = SKPaintStyle.Fill - }; - canvas.DrawRect(0, 0, width, height, paintColor); - - var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); - - // use the system fallback to find a typeface for the given CJK character - var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]"; - var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty); - if (!string.IsNullOrEmpty(filteredName)) - { - typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]); - } - - // draw library name - using var textPaint = new SKPaint - { - Color = SKColors.White, - Style = SKPaintStyle.Fill, - TextSize = 112, - TextAlign = SKTextAlign.Center, - Typeface = typeFace, - IsAntialias = true - }; - - // scale down text to 90% of the width if text is larger than 95% of the width - var textWidth = textPaint.MeasureText(libraryName); - if (textWidth > width * 0.95) - { - textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth; - } - - canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); - return bitmap; } - private SKBitmap BuildSquareCollageBitmap(IReadOnlyList paths, int width, int height) - { - var bitmap = new SKBitmap(width, height); - var imageIndex = 0; - var cellWidth = width / 2; - var cellHeight = height / 2; + // resize to the same aspect as the original + var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width); + using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace)); + // draw the backdrop + canvas.DrawImage(residedBackdrop, 0, 0); - using var canvas = new SKCanvas(bitmap); - for (var x = 0; x < 2; x++) + // draw shadow rectangle + using var paintColor = new SKPaint + { + Color = SKColors.Black.WithAlpha(0x78), + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(0, 0, width, height, paintColor); + + var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); + + // use the system fallback to find a typeface for the given CJK character + var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]"; + var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty); + if (!string.IsNullOrEmpty(filteredName)) + { + typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]); + } + + // draw library name + using var textPaint = new SKPaint + { + Color = SKColors.White, + Style = SKPaintStyle.Fill, + TextSize = 112, + TextAlign = SKTextAlign.Center, + Typeface = typeFace, + IsAntialias = true + }; + + // scale down text to 90% of the width if text is larger than 95% of the width + var textWidth = textPaint.MeasureText(libraryName); + if (textWidth > width * 0.95) + { + textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth; + } + + canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + + return bitmap; + } + + private SKBitmap BuildSquareCollageBitmap(IReadOnlyList paths, int width, int height) + { + var bitmap = new SKBitmap(width, height); + var imageIndex = 0; + var cellWidth = width / 2; + var cellHeight = height / 2; + + using var canvas = new SKCanvas(bitmap); + for (var x = 0; x < 2; x++) + { + for (var y = 0; y < 2; y++) { - for (var y = 0; y < 2; y++) + using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex); + imageIndex = newIndex; + + if (currentBitmap is null) { - using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex); - imageIndex = newIndex; - - if (currentBitmap is null) - { - continue; - } - - // Scale image. The FromBitmap creates a copy - var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace); - using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo)); - - // draw this image into the strip at the next position - var xPos = x * cellWidth; - var yPos = y * cellHeight; - canvas.DrawBitmap(resizedBitmap, xPos, yPos); + continue; } - } - return bitmap; + // Scale image. The FromBitmap creates a copy + var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace); + using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo)); + + // draw this image into the strip at the next position + var xPos = x * cellWidth; + var yPos = y * cellHeight; + canvas.DrawBitmap(resizedBitmap, xPos, yPos); + } } + + return bitmap; } } diff --git a/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs index 58f887c960..456b84b8c8 100644 --- a/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs +++ b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs @@ -2,63 +2,62 @@ using System.Globalization; using MediaBrowser.Model.Drawing; using SkiaSharp; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// Static helper class for drawing unplayed count indicators. +/// +public static class UnplayedCountIndicator { /// - /// Static helper class for drawing unplayed count indicators. + /// The x-offset used when drawing an unplayed count indicator. /// - public static class UnplayedCountIndicator + private const int OffsetFromTopRightCorner = 38; + + /// + /// Draw an unplayed count indicator in the top right corner of a canvas. + /// + /// The canvas to draw the indicator on. + /// + /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the + /// indicator. + /// + /// The number to draw in the indicator. + public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count) { - /// - /// The x-offset used when drawing an unplayed count indicator. - /// - private const int OffsetFromTopRightCorner = 38; + var x = imageSize.Width - OffsetFromTopRightCorner; + var text = count.ToString(CultureInfo.InvariantCulture); - /// - /// Draw an unplayed count indicator in the top right corner of a canvas. - /// - /// The canvas to draw the indicator on. - /// - /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the - /// indicator. - /// - /// The number to draw in the indicator. - public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count) + using var paint = new SKPaint { - var x = imageSize.Width - OffsetFromTopRightCorner; - var text = count.ToString(CultureInfo.InvariantCulture); + Color = SKColor.Parse("#CC00A4DC"), + Style = SKPaintStyle.Fill + }; - using var paint = new SKPaint - { - Color = SKColor.Parse("#CC00A4DC"), - Style = SKPaintStyle.Fill - }; + canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); - canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); + paint.Color = new SKColor(255, 255, 255, 255); + paint.TextSize = 24; + paint.IsAntialias = true; - paint.Color = new SKColor(255, 255, 255, 255); - paint.TextSize = 24; - paint.IsAntialias = true; + var y = OffsetFromTopRightCorner + 9; - var y = OffsetFromTopRightCorner + 9; - - if (text.Length == 1) - { - x -= 7; - } - - if (text.Length == 2) - { - x -= 13; - } - else if (text.Length >= 3) - { - x -= 15; - y -= 2; - paint.TextSize = 18; - } - - canvas.DrawText(text, x, y, paint); + if (text.Length == 1) + { + x -= 7; } + + if (text.Length == 2) + { + x -= 13; + } + else if (text.Length >= 3) + { + x -= 15; + y -= 2; + paint.TextSize = 18; + } + + canvas.DrawText(text, x, y, paint); } } From 6dbdb4e9af9de2323594d9269fdaacdf38c3b940 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 11 Jan 2023 09:55:05 +0100 Subject: [PATCH 55/97] Fix all warnings in Jellyfin.Server.Implementations --- .../Jellyfin.Server.Implementations.csproj | 4 ---- .../Users/DefaultPasswordResetProvider.cs | 6 ++++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index c2c3513ad9..bc437c5d77 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -6,10 +6,6 @@ true - - false - - diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index 4fda8f5a41..9601954671 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -54,7 +54,8 @@ namespace Jellyfin.Server.Implementations.Users foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) { SerializablePasswordReset spr; - await using (var str = AsyncFile.OpenRead(resetFile)) + var str = AsyncFile.OpenRead(resetFile); + await using (str.ConfigureAwait(false)) { spr = await JsonSerializer.DeserializeAsync(str).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid."); @@ -107,7 +108,8 @@ namespace Jellyfin.Server.Implementations.Users UserName = user.Username }; - await using (FileStream fileStream = AsyncFile.OpenWrite(filePath)) + FileStream fileStream = AsyncFile.OpenWrite(filePath); + await using (fileStream.ConfigureAwait(false)) { await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false); } From b934b346e1f2a78ece62e0248f55fcb6a8e8ff2a Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 11 Jan 2023 10:36:18 +0100 Subject: [PATCH 56/97] Fix all warnings in MediaBrowser.Providers --- MediaBrowser.Providers/Manager/ImageSaver.cs | 3 +- .../Manager/ItemImageProvider.cs | 40 +++++++++------- .../Manager/ProviderManager.cs | 19 ++++---- .../MediaBrowser.Providers.csproj | 4 -- .../MediaInfo/EmbeddedImageProvider.cs | 2 +- .../MediaInfo/FFProbeVideoInfo.cs | 4 +- .../AudioDb/AudioDbAlbumImageProvider.cs | 23 ++++----- .../Plugins/AudioDb/AudioDbAlbumProvider.cs | 36 ++++++++------ .../AudioDb/AudioDbArtistImageProvider.cs | 13 +++-- .../Plugins/AudioDb/AudioDbArtistProvider.cs | 40 +++++++++------- .../Plugins/Omdb/OmdbItemProvider.cs | 36 +++++++------- .../Plugins/Omdb/OmdbProvider.cs | 28 +++++++---- .../StudioImages/StudiosImageProvider.cs | 12 +++-- .../Tmdb/BoxSets/TmdbBoxSetImageProvider.cs | 4 +- .../Tmdb/Movies/TmdbMovieImageProvider.cs | 6 +-- .../Tmdb/People/TmdbPersonImageProvider.cs | 7 +-- .../Tmdb/TV/TmdbEpisodeImageProvider.cs | 6 +-- .../Tmdb/TV/TmdbSeasonImageProvider.cs | 6 +-- .../Tmdb/TV/TmdbSeriesImageProvider.cs | 6 +-- .../Plugins/Tmdb/TmdbClientManager.cs | 48 ++++++++----------- .../Subtitles/SubtitleManager.cs | 14 ++++-- 21 files changed, 193 insertions(+), 164 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 50e7040607..e7c2cd2558 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -264,7 +264,8 @@ namespace MediaBrowser.Providers.Manager var fileStreamOptions = AsyncFile.WriteOptions; fileStreamOptions.Mode = FileMode.Create; fileStreamOptions.PreallocationSize = source.Length; - await using (var fs = new FileStream(path, fileStreamOptions)) + var fs = new FileStream(path, fileStreamOptions); + await using (fs.ConfigureAwait(false)) { await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index a0f48840e3..d621555f13 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -502,15 +502,17 @@ namespace MediaBrowser.Providers.Manager break; } - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - - await _providerManager.SaveImage( - item, - stream, - response.Content.Headers.ContentType?.MediaType, - type, - null, - cancellationToken).ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + await _providerManager.SaveImage( + item, + stream, + response.Content.Headers.ContentType?.MediaType, + type, + null, + cancellationToken).ConfigureAwait(false); + } result.UpdateType |= ItemUpdateType.ImageUpdate; return true; @@ -626,14 +628,18 @@ namespace MediaBrowser.Providers.Manager } } - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await _providerManager.SaveImage( - item, - stream, - response.Content.Headers.ContentType?.MediaType, - imageType, - null, - cancellationToken).ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + await _providerManager.SaveImage( + item, + stream, + response.Content.Headers.ContentType?.MediaType, + imageType, + null, + cancellationToken).ConfigureAwait(false); + } + result.UpdateType |= ItemUpdateType.ImageUpdate; } catch (HttpRequestException) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 914da33a96..0ce696edc6 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -182,14 +182,17 @@ namespace MediaBrowser.Providers.Manager contentType = MimeTypes.GetMimeType(url); } - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await SaveImage( - item, - stream, - contentType, - type, - imageIndex, - cancellationToken).ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + await SaveImage( + item, + stream, + contentType, + type, + imageIndex, + cancellationToken).ConfigureAwait(false); + } } /// diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9713e02297..97ad1ffbcb 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -34,10 +34,6 @@ ../jellyfin.ruleset - - false - - diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index fed23df152..f58f5f7a33 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -140,7 +140,7 @@ namespace MediaBrowser.Providers.MediaInfo if (attachmentStream is not null) { - return await ExtractAttachment(item, attachmentStream, mediaSource, cancellationToken); + return await ExtractAttachment(item, attachmentStream, mediaSource, cancellationToken).ConfigureAwait(false); } // Fall back to EmbeddedImage streams diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 751135a2c8..81434b8620 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -557,7 +557,7 @@ namespace MediaBrowser.Providers.MediaInfo CancellationToken cancellationToken) { var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1); - var externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken); + var externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken).ConfigureAwait(false); var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh; @@ -611,7 +611,7 @@ namespace MediaBrowser.Providers.MediaInfo // Rescan if (downloadedLanguages.Count > 0) { - externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken); + externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs index 7fb438d8a0..7f73afc53a 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; @@ -42,11 +43,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public IEnumerable GetSupportedImages(BaseItem item) { - return new List - { - ImageType.Primary, - ImageType.Disc - }; + yield return ImageType.Primary; + yield return ImageType.Disc; } /// @@ -60,16 +58,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id); - await using FileStream jsonStream = AsyncFile.OpenRead(path); - var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - - if (obj is not null && obj.album is not null && obj.album.Count > 0) + FileStream jsonStream = AsyncFile.OpenRead(path); + await using (jsonStream.ConfigureAwait(false)) { - return GetImages(obj.album[0]); + var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + + if (obj is not null && obj.album is not null && obj.album.Count > 0) + { + return GetImages(obj.album[0]); + } } } - return new List(); + return Enumerable.Empty(); } private IEnumerable GetImages(AudioDbAlbumProvider.Album item) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index b92f1f59f7..55e2474a5a 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -68,14 +68,17 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = GetAlbumInfoPath(_config.ApplicationPaths, id); - await using FileStream jsonStream = AsyncFile.OpenRead(path); - var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - - if (obj is not null && obj.album is not null && obj.album.Count > 0) + FileStream jsonStream = AsyncFile.OpenRead(path); + await using (jsonStream.ConfigureAwait(false)) { - result.Item = new MusicAlbum(); - result.HasMetadata = true; - ProcessResult(result.Item, obj.album[0], info.MetadataLanguage); + var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + + if (obj is not null && obj.album is not null && obj.album.Count > 0) + { + result.Item = new MusicAlbum(); + result.HasMetadata = true; + ProcessResult(result.Item, obj.album[0], info.MetadataLanguage); + } } } @@ -173,13 +176,18 @@ namespace MediaBrowser.Providers.Plugins.AudioDb Directory.CreateDirectory(Path.GetDirectoryName(path)); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - - var fileStreamOptions = AsyncFile.WriteOptions; - fileStreamOptions.Mode = FileMode.Create; - fileStreamOptions.PreallocationSize = stream.Length; - await using var xmlFileStream = new FileStream(path, fileStreamOptions); - await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + var fileStreamOptions = AsyncFile.WriteOptions; + fileStreamOptions.Mode = FileMode.Create; + fileStreamOptions.PreallocationSize = stream.Length; + var xmlFileStream = new FileStream(path, fileStreamOptions); + await using (xmlFileStream.ConfigureAwait(false)) + { + await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); + } + } } private static string GetAlbumDataPath(IApplicationPaths appPaths, string musicBrainzReleaseGroupId) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs index 6d67ad634a..b1a285a964 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs @@ -62,12 +62,15 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id); - await using FileStream jsonStream = AsyncFile.OpenRead(path); - var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - - if (obj is not null && obj.artists is not null && obj.artists.Count > 0) + FileStream jsonStream = AsyncFile.OpenRead(path); + await using (jsonStream.ConfigureAwait(false)) { - return GetImages(obj.artists[0]); + var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + + if (obj is not null && obj.artists is not null && obj.artists.Count > 0) + { + return GetImages(obj.artists[0]); + } } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 1565a8c515..f3385b3a91 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -67,14 +67,17 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = GetArtistInfoPath(_config.ApplicationPaths, id); - await using FileStream jsonStream = AsyncFile.OpenRead(path); - var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - - if (obj is not null && obj.artists is not null && obj.artists.Count > 0) + FileStream jsonStream = AsyncFile.OpenRead(path); + await using (jsonStream.ConfigureAwait(false)) { - result.Item = new MusicArtist(); - result.HasMetadata = true; - ProcessResult(result.Item, obj.artists[0], info.MetadataLanguage); + var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + + if (obj is not null && obj.artists is not null && obj.artists.Count > 0) + { + result.Item = new MusicArtist(); + result.HasMetadata = true; + ProcessResult(result.Item, obj.artists[0], info.MetadataLanguage); + } } } @@ -151,16 +154,21 @@ namespace MediaBrowser.Providers.Plugins.AudioDb using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); + Directory.CreateDirectory(Path.GetDirectoryName(path)); - var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - var fileStreamOptions = AsyncFile.WriteOptions; - fileStreamOptions.Mode = FileMode.Create; - fileStreamOptions.PreallocationSize = stream.Length; - await using var xmlFileStream = new FileStream(path, fileStreamOptions); - await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); + var fileStreamOptions = AsyncFile.WriteOptions; + fileStreamOptions.Mode = FileMode.Create; + fileStreamOptions.PreallocationSize = stream.Length; + var xmlFileStream = new FileStream(path, fileStreamOptions); + await using (xmlFileStream.ConfigureAwait(false)) + { + await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); + } + } } /// diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 3ef94ca93b..e4bb4eaead 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -137,29 +137,31 @@ namespace MediaBrowser.Providers.Plugins.Omdb var url = OmdbProvider.GetOmdbUrl(urlQuery.ToString()); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - - if (isSearch) + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) { - var searchResultList = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (searchResultList?.Search is not null) + if (isSearch) { - var resultCount = searchResultList.Search.Count; - var result = new RemoteSearchResult[resultCount]; - for (var i = 0; i < resultCount; i++) + var searchResultList = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + if (searchResultList?.Search is not null) { - result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd); - } + var resultCount = searchResultList.Search.Count; + var result = new RemoteSearchResult[resultCount]; + for (var i = 0; i < resultCount; i++) + { + result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd); + } - return result; + return result; + } } - } - else - { - var result = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase)) + else { - return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) }; + var result = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase)) + { + return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) }; + } } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 6713a34e61..497437bd8a 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -234,15 +234,21 @@ namespace MediaBrowser.Providers.Plugins.Omdb internal async Task GetRootObject(string imdbId, CancellationToken cancellationToken) { var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false); - await using var stream = AsyncFile.OpenRead(path); - return await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var stream = AsyncFile.OpenRead(path); + await using (stream.ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + } } internal async Task GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken) { var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false); - await using var stream = AsyncFile.OpenRead(path); - return await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var stream = AsyncFile.OpenRead(path); + await using (stream.ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + } } /// Gets OMDB URL. @@ -317,8 +323,11 @@ namespace MediaBrowser.Providers.Plugins.Omdb imdbParam)); var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync(url, _jsonOptions, cancellationToken).ConfigureAwait(false); - await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); + FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await using (jsonFileStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); + } return path; } @@ -357,8 +366,11 @@ namespace MediaBrowser.Providers.Plugins.Omdb seasonId)); var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync(url, _jsonOptions, cancellationToken).ConfigureAwait(false); - await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); + FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await using (jsonFileStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); + } return path; } diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs index 4ff9e02477..0fb9d30a62 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs @@ -138,9 +138,15 @@ namespace MediaBrowser.Providers.Plugins.StudioImages var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); Directory.CreateDirectory(Path.GetDirectoryName(file)); - await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); + var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false); + await using (response.ConfigureAwait(false)) + { + var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await using (fileStream.ConfigureAwait(false)) + { + await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); + } + } } return file; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index 20898d213d..eee3658de5 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -81,8 +81,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets var backdrops = collection.Images.Backdrops; var remoteImages = new List(posters.Count + backdrops.Count); - _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages); - _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages); + remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); + remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); return remoteImages; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs index 01b8bca397..02601d3f56 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs @@ -100,9 +100,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var logos = movie.Images.Logos; var remoteImages = new List(posters.Count + backdrops.Count + logos.Count); - _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages); - _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages); - _tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language, remoteImages); + remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); + remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); + remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language)); return remoteImages; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index aa46d8f257..bc959ee2bd 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -69,12 +69,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return Enumerable.Empty(); } - var profiles = personResult.Images.Profiles; - var remoteImages = new List(profiles.Count); - - _tmdbClientManager.ConvertProfilesToRemoteImageInfo(profiles, language, remoteImages); - - return remoteImages; + return _tmdbClientManager.ConvertProfilesToRemoteImageInfo(personResult.Images.Profiles, language); } /// diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index 127d41cc7d..5259faf76f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -89,11 +89,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return Enumerable.Empty(); } - var remoteImages = new List(stills.Count); - - _tmdbClientManager.ConvertStillsToRemoteImageInfo(stills, language, remoteImages); - - return remoteImages; + return _tmdbClientManager.ConvertStillsToRemoteImageInfo(stills, language); } /// diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index fda00537d5..b8d1460db9 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -80,11 +80,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return Enumerable.Empty(); } - var remoteImages = new List(posters.Count); - - _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages); - - return remoteImages; + return _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language); } /// diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 9062f1b850..79cb6e86d4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -83,9 +83,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var logos = series.Images.Logos; var remoteImages = new List(posters.Count + backdrops.Count + logos.Count); - _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages); - _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages); - _tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language, remoteImages); + remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); + remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); + remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language)); return remoteImages; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index b56c0d7482..c7441bf357 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -531,55 +531,45 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// /// The input images. /// The requested language. - /// The collection to add the remote images into. - public void ConvertPostersToRemoteImageInfo(List images, string requestLanguage, List results) - { - ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLanguage, results); - } + /// The remote images. + public IEnumerable ConvertPostersToRemoteImageInfo(IReadOnlyList images, string requestLanguage) + => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLanguage); /// /// Converts backdrop s into s. /// /// The input images. /// The requested language. - /// The collection to add the remote images into. - public void ConvertBackdropsToRemoteImageInfo(List images, string requestLanguage, List results) - { - ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestLanguage, results); - } + /// The remote images. + public IEnumerable ConvertBackdropsToRemoteImageInfo(IReadOnlyList images, string requestLanguage) + => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestLanguage); /// /// Converts logo s into s. /// /// The input images. /// The requested language. - /// The collection to add the remote images into. - public void ConvertLogosToRemoteImageInfo(List images, string requestLanguage, List results) - { - ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.LogoSize, ImageType.Logo, requestLanguage, results); - } + /// The remote images. + public IEnumerable ConvertLogosToRemoteImageInfo(IReadOnlyList images, string requestLanguage) + => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.LogoSize, ImageType.Logo, requestLanguage); /// /// Converts profile s into s. /// /// The input images. /// The requested language. - /// The collection to add the remote images into. - public void ConvertProfilesToRemoteImageInfo(List images, string requestLanguage, List results) - { - ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLanguage, results); - } + /// The remote images. + public IEnumerable ConvertProfilesToRemoteImageInfo(IReadOnlyList images, string requestLanguage) + => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLanguage); /// /// Converts still s into s. /// /// The input images. /// The requested language. - /// The collection to add the remote images into. - public void ConvertStillsToRemoteImageInfo(List images, string requestLanguage, List results) - { - ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLanguage, results); - } + /// The remote images. + public IEnumerable ConvertStillsToRemoteImageInfo(IReadOnlyList images, string requestLanguage) + => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLanguage); /// /// Converts s into s. @@ -588,8 +578,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// The size of the image to fetch. /// The type of the image. /// The requested language. - /// The collection to add the remote images into. - private void ConvertToRemoteImageInfo(List images, string size, ImageType type, string requestLanguage, List results) + /// The remote images. + private IEnumerable ConvertToRemoteImageInfo(IReadOnlyList images, string size, ImageType type, string requestLanguage) { // sizes provided are for original resolution, don't store them when downloading scaled images var scaleImage = !string.Equals(size, "original", StringComparison.OrdinalIgnoreCase); @@ -598,7 +588,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb { var image = images[i]; - results.Add(new RemoteImageInfo + yield return new RemoteImageInfo { Url = GetUrl(size, image.FilePath), CommunityRating = image.VoteAverage, @@ -609,7 +599,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb ProviderName = TmdbUtils.ProviderName, Type = type, RatingType = RatingType.Score - }); + }; } } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 1aeffb65fc..b1a26cfba3 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -188,10 +188,16 @@ namespace MediaBrowser.Providers.Subtitles { var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia; - await using var stream = response.Stream; - await using var memoryStream = new MemoryStream(); - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; + var memoryStream = new MemoryStream(); + await using (memoryStream.ConfigureAwait(false)) + { + var stream = response.Stream; + await using (stream.ConfigureAwait(false)) + { + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + } + } var savePaths = new List(); var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); From ab645653bc0b6fbc071832f8f27722225e6d5234 Mon Sep 17 00:00:00 2001 From: 0TTA Date: Tue, 10 Jan 2023 11:30:18 +0000 Subject: [PATCH 57/97] Translated using Weblate (Arabic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/ --- .../Localization/Core/ar.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 4508363b0c..93d50e6e3b 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -3,9 +3,9 @@ "AppDeviceValues": "تطبيق: {0}, جهاز: {1}", "Application": "تطبيق", "Artists": "الفنانين", - "AuthenticationSucceededWithUserName": "تمت مصادقة {0} بنجاح", + "AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}", "Books": "الكتب", - "CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}", + "CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}", "Channels": "القنوات", "ChapterNameValue": "الفصل {0}", "Collections": "التجميعات", @@ -16,7 +16,7 @@ "Folders": "المجلدات", "Genres": "التصنيفات", "HeaderAlbumArtists": "فناني الألبوم", - "HeaderContinueWatching": "استمر بالمشاهدة", + "HeaderContinueWatching": "استئناف المشاهدة", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteEpisodes": "الحلقات المفضلة", @@ -27,15 +27,15 @@ "HeaderRecordingGroups": "مجموعات التسجيل", "HomeVideos": "الفيديوهات الشخصية", "Inherit": "توريث", - "ItemAddedWithName": "تم إضافة {0} للمكتبة", - "ItemRemovedWithName": "تم إزالة {0} من المكتبة", + "ItemAddedWithName": "أُضيف {0} للمكتبة", + "ItemRemovedWithName": "أُزيل {0} من المكتبة", "LabelIpAddressValue": "عنوان الآي بي: {0}", "LabelRunningTimeValue": "مدة التشغيل: {0}", "Latest": "أحدث", - "MessageApplicationUpdated": "لقد تم تحديث خادم Jellyfin", - "MessageApplicationUpdatedTo": "تم تحديث خادم Jellyfin الى {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث إعدادات الخادم في قسم {0}", - "MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم", + "MessageApplicationUpdated": "حُدث خادم Jellyfin", + "MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}", + "MessageServerConfigurationUpdated": "حُدثت إعدادات الخادم", "MixedContent": "محتوى مختلط", "Movies": "الأفلام", "Music": "الموسيقى", @@ -45,14 +45,14 @@ "NameSeasonUnknown": "الموسم غير معروف", "NewVersionIsAvailable": "نسخة جديدة من خادم Jellyfin متوفرة للتحميل.", "NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق", - "NotificationOptionApplicationUpdateInstalled": "تم تحديث التطبيق", + "NotificationOptionApplicationUpdateInstalled": "نُصب تحديث التطبيق", "NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي", - "NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي", - "NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا", + "NotificationOptionAudioPlaybackStopped": "أُوقف تشغيل المقطع الصوتي", + "NotificationOptionCameraImageUploaded": "رُفعت صورة الكاميرا", "NotificationOptionInstallationFailed": "فشل في التثبيت", - "NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد", + "NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا", "NotificationOptionPluginError": "فشل في الملحق", - "NotificationOptionPluginInstalled": "تم تثبيت الملحق", + "NotificationOptionPluginInstalled": "ثُبتت المكونات الإضافية", "NotificationOptionPluginUninstalled": "تمت إزالة الملحق", "NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق", "NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم", From 81c8890b6d0f8b84dd52264d6c43e04dc130c79d Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 12 Jan 2023 01:22:01 +0100 Subject: [PATCH 58/97] Fix all warnings in MediaBrowser.MediaEncoding (#9073) --- .../Encoder/MediaEncoder.cs | 5 ++- .../MediaBrowser.MediaEncoding.csproj | 4 -- .../Subtitles/SubtitleEncoder.cs | 44 ++++++++++++------- .../Subtitles/SubtitleEncoderTests.cs | 34 +++++++++++--- 4 files changed, 60 insertions(+), 27 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 91bf42b150..d95f894c52 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -498,11 +498,12 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args); + var memoryStream = new MemoryStream(); + await using (memoryStream.ConfigureAwait(false)) using (var processWrapper = new ProcessWrapper(process, this)) { - await using var memoryStream = new MemoryStream(); StartProcess(processWrapper); - await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken); + await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); memoryStream.Seek(0, SeekOrigin.Begin); InternalMediaInfoResult result; try diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 7404c2868b..e33cfc7a12 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -11,10 +11,6 @@ true - - false - - diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index b7c2fd7b12..90bc491322 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -226,7 +226,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, cancellationToken) .ConfigureAwait(false); - return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false); + return new SubtitleInfo() + { + Path = outputPath, + Protocol = MediaProtocol.File, + Format = outputFormat, + IsExternal = false + }; } var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) @@ -240,11 +246,23 @@ namespace MediaBrowser.MediaEncoding.Subtitles await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); - return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true); + return new SubtitleInfo() + { + Path = outputPath, + Protocol = MediaProtocol.File, + Format = "srt", + IsExternal = true + }; } // It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs) - return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true); + return new SubtitleInfo() + { + Path = subtitleStream.Path, + Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path), + Format = currentFormat, + IsExternal = true + }; } private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value) @@ -728,23 +746,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - public readonly struct SubtitleInfo +#pragma warning disable CA1034 // Nested types should not be visible + // Only public for the unit tests + public readonly record struct SubtitleInfo { - public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal) - { - Path = path; - Protocol = protocol; - Format = format; - IsExternal = isExternal; - } + public string Path { get; init; } - public string Path { get; } + public MediaProtocol Protocol { get; init; } - public MediaProtocol Protocol { get; } + public string Format { get; init; } - public string Format { get; } - - public bool IsExternal { get; } + public bool IsExternal { get; init; } } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs index 2431274383..9ace80bbd2 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs @@ -26,7 +26,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests Path = "/media/sub.ass", IsExternal = true }, - new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true)); + new SubtitleEncoder.SubtitleInfo() + { + Path = "/media/sub.ass", + Protocol = MediaProtocol.File, + Format = "ass", + IsExternal = true + }); data.Add( new MediaSourceInfo() @@ -38,7 +44,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests Path = "/media/sub.ssa", IsExternal = true }, - new SubtitleEncoder.SubtitleInfo("/media/sub.ssa", MediaProtocol.File, "ssa", true)); + new SubtitleEncoder.SubtitleInfo() + { + Path = "/media/sub.ssa", + Protocol = MediaProtocol.File, + Format = "ssa", + IsExternal = true + }); data.Add( new MediaSourceInfo() @@ -50,7 +62,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests Path = "/media/sub.srt", IsExternal = true }, - new SubtitleEncoder.SubtitleInfo("/media/sub.srt", MediaProtocol.File, "srt", true)); + new SubtitleEncoder.SubtitleInfo() + { + Path = "/media/sub.srt", + Protocol = MediaProtocol.File, + Format = "srt", + IsExternal = true + }); data.Add( new MediaSourceInfo() @@ -62,14 +80,20 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests Path = "/media/sub.ass", IsExternal = true }, - new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true)); + new SubtitleEncoder.SubtitleInfo() + { + Path = "/media/sub.ass", + Protocol = MediaProtocol.File, + Format = "ass", + IsExternal = true + }); return data; } [Theory] [MemberData(nameof(GetReadableFile_Valid_TestData))] - internal async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo) + public async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo) { var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); var subtitleEncoder = fixture.Create(); From 7b17799b013fd8bc76a51b60d670a83901607e86 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Wed, 11 Jan 2023 22:07:41 -0500 Subject: [PATCH 59/97] Migrate from IWebHost to IHost --- Jellyfin.Server/Program.cs | 37 ++++++++----------- Jellyfin.Server/Startup.cs | 16 +++----- .../JellyfinApplicationFactory.cs | 2 +- 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 7052f4d2bf..4725005ae6 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Net; using System.Reflection; -using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Threading; @@ -21,7 +20,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -186,20 +184,26 @@ namespace Jellyfin.Server try { - var serviceCollection = new ServiceCollection(); - appHost.Init(serviceCollection); + var host = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + // NOTE: Called first to ensure app host configuration is fully initialized + appHost.Init(services); + }) + .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths)) + .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) + .UseSerilog() + .Build(); - var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); - - // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection. - appHost.ServiceProvider = webHost.Services; + // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. + appHost.ServiceProvider = host.Services; await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); try { - await webHost.StartAsync(_tokenSource.Token).ConfigureAwait(false); + await host.StartAsync(_tokenSource.Token).ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -284,16 +288,12 @@ namespace Jellyfin.Server /// /// The builder to configure. /// The application host. - /// The application service collection. - /// The command line options passed to the application. /// The application configuration. /// The application paths. /// The configured web host builder. public static IWebHostBuilder ConfigureWebHostBuilder( this IWebHostBuilder builder, - ApplicationHost appHost, - IServiceCollection serviceCollection, - StartupOptions commandLineOpts, + CoreAppHost appHost, IConfiguration startupConfig, IApplicationPaths appPaths) { @@ -349,14 +349,7 @@ namespace Jellyfin.Server _logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); } }) - .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig)) - .UseSerilog() - .ConfigureServices(services => - { - // Merge the external ServiceCollection into ASP.NET DI - services.Add(serviceCollection); - }) - .UseStartup(); + .UseStartup(_ => new Startup(appHost)); } /// diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 5d6a278c40..5996b3e305 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -35,20 +35,17 @@ namespace Jellyfin.Server /// public class Startup { - private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerApplicationHost _serverApplicationHost; + private readonly IServerConfigurationManager _serverConfigurationManager; /// /// Initializes a new instance of the class. /// - /// The server configuration manager. - /// The server application host. - public Startup( - IServerConfigurationManager serverConfigurationManager, - IServerApplicationHost serverApplicationHost) + /// The server application host. + public Startup(CoreAppHost appHost) { - _serverConfigurationManager = serverConfigurationManager; - _serverApplicationHost = serverApplicationHost; + _serverApplicationHost = appHost; + _serverConfigurationManager = appHost.ConfigurationManager; } /// @@ -87,8 +84,7 @@ namespace Jellyfin.Server RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8 }; - services - .AddHttpClient(NamedClient.Default, c => + services.AddHttpClient(NamedClient.Default, c => { c.DefaultRequestHeaders.UserAgent.Add(productHeader); c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader); diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index c38faeda17..41b2273d04 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -82,7 +82,7 @@ namespace Jellyfin.Server.Integration.Tests appHost.Init(serviceCollection); // Configure the web host builder - Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths); + Program.ConfigureWebHostBuilder(builder, appHost, startupConfig, appPaths); } /// From 159e74ea09b623ab422c7000cb3621abd8de1118 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Wed, 11 Jan 2023 22:08:04 -0500 Subject: [PATCH 60/97] Update Serilog.AspNetCore --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 195d7f3a98..b16c566d5b 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -41,7 +41,7 @@ - + From 0f46eca6a4e9f4b400c7a08defd033ce834e2974 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Wed, 11 Jan 2023 22:09:14 -0500 Subject: [PATCH 61/97] Minor cleanup in Startup class --- Jellyfin.Server/Startup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 5996b3e305..f89f81c766 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -204,7 +204,7 @@ namespace Jellyfin.Server endpoints.MapControllers(); if (_serverConfigurationManager.Configuration.EnableMetrics) { - endpoints.MapMetrics("/metrics"); + endpoints.MapMetrics(); } endpoints.MapHealthChecks("/health"); From 8478a7c9ed014cf89d2e25328e3c8007c0e5dff7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Jan 2023 08:40:36 -0700 Subject: [PATCH 62/97] chore(deps): update github/codeql-action digest to 515828d (#9079) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7153d4cf5f..238e31f4b4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2 + uses: github/codeql-action/init@515828d97454b8354517688ddc5b48402b723750 # v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2 + uses: github/codeql-action/autobuild@515828d97454b8354517688ddc5b48402b723750 # v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2 + uses: github/codeql-action/analyze@515828d97454b8354517688ddc5b48402b723750 # v2 From 033ffa9a8899be7f28e72603cf5881c066b40cd5 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 12 Jan 2023 11:51:12 -0500 Subject: [PATCH 63/97] Fix tests --- Jellyfin.Server/Program.cs | 6 +----- .../JellyfinApplicationFactory.cs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 4725005ae6..1506530f0c 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -185,11 +185,7 @@ namespace Jellyfin.Server try { var host = Host.CreateDefaultBuilder() - .ConfigureServices(services => - { - // NOTE: Called first to ensure app host configuration is fully initialized - appHost.Init(services); - }) + .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) .UseSerilog() diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 41b2273d04..1bfa5996d8 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -8,6 +8,7 @@ using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; @@ -78,11 +79,17 @@ namespace Jellyfin.Server.Integration.Tests commandLineOpts, startupConfig); _disposableComponents.Add(appHost); - var serviceCollection = new ServiceCollection(); - appHost.Init(serviceCollection); - // Configure the web host builder - Program.ConfigureWebHostBuilder(builder, appHost, startupConfig, appPaths); + builder.ConfigureServices(services => appHost.Init(services)) + .ConfigureWebHostBuilder(appHost, startupConfig, appPaths) + .ConfigureAppConfiguration((context, builder) => + { + builder + .SetBasePath(appPaths.ConfigurationDirectoryPath) + .AddInMemoryCollection(ConfigurationOptions.DefaultConfiguration) + .AddEnvironmentVariables("JELLYFIN_") + .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); + }); } /// From d39dcb8ad2433a93524660a0cf47684a03f9cdd5 Mon Sep 17 00:00:00 2001 From: kshantum Date: Thu, 12 Jan 2023 04:05:02 +0000 Subject: [PATCH 64/97] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/ --- Emby.Server.Implementations/Localization/Core/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index dc45a8264d..65cf29e807 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -1,6 +1,6 @@ { "Albums": "Альбомы", - "AppDeviceValues": "Приложение.: {0}, Устройство.: {1}", + "AppDeviceValues": "Приложение: {0}, Устройство: {1}", "Application": "Приложение", "Artists": "Исполнители", "AuthenticationSucceededWithUserName": "{0} - авторизация успешна", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlaybackStopped": "Воспроизведение аудио остановлено", "NotificationOptionCameraImageUploaded": "Изображения с камеры загружены", "NotificationOptionInstallationFailed": "Сбой установки", - "NotificationOptionNewLibraryContent": "Новое содержание добавлено", + "NotificationOptionNewLibraryContent": "Новое содержимое добавлено", "NotificationOptionPluginError": "Сбой плагина", "NotificationOptionPluginInstalled": "Плагин установлен", "NotificationOptionPluginUninstalled": "Плагин удалён", From 0571d3e403906167ce4204e63f8697f97e35d0bf Mon Sep 17 00:00:00 2001 From: Alex Popovic Date: Wed, 11 Jan 2023 22:25:39 +0000 Subject: [PATCH 65/97] Translated using Weblate (Serbian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sr/ --- Emby.Server.Implementations/Localization/Core/sr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index 1be8867f47..9739358df0 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.", "External": "Спољно", "TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.", - "TaskKeyframeExtractor": "Екстрактор кључних сличица" + "TaskKeyframeExtractor": "Екстрактор кључних сличица", + "HearingImpaired": "ослабљен слух" } From 24a27a360ee872426f9db3b701271e06f90d19bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Jan 2023 06:09:30 -0700 Subject: [PATCH 66/97] chore(deps): update dependency serilog.aspnetcore to v6.1.0 (#9085) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index b16c566d5b..829f294ce3 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -41,7 +41,7 @@ - + From 3d51d79715a1cbc0e6c237facfdc9f21c4b951c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Jan 2023 08:09:21 -0700 Subject: [PATCH 67/97] chore(deps): update swashbuckle-aspnetcore monorepo to v6.5.0 (#8591) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Jellyfin.Api/Jellyfin.Api.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index b5444138fb..0031886d71 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -15,8 +15,8 @@ - - + + From 0f17e72efd12da7c7fd971e992076d24c18f8f2a Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Fri, 13 Jan 2023 16:31:36 -0700 Subject: [PATCH 68/97] Revert "chore(deps): update swashbuckle-aspnetcore monorepo to v6.5.0 (#8591)" (#9088) This reverts commit 3d51d79715a1cbc0e6c237facfdc9f21c4b951c7. --- Jellyfin.Api/Jellyfin.Api.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 0031886d71..b5444138fb 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -15,8 +15,8 @@ - - + + From 37edb2188782fcae25175767e6713c64cf24b5a5 Mon Sep 17 00:00:00 2001 From: Napaul Intrarasing Date: Fri, 13 Jan 2023 15:11:16 +0000 Subject: [PATCH 69/97] Translated using Weblate (Thai) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/th/ --- Emby.Server.Implementations/Localization/Core/th.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 9407a7b921..1a4fef64e8 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -120,5 +120,6 @@ "Forced": "บังคับใช้", "TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล", "TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น", - "External": "ภายนอก" + "External": "ภายนอก", + "HearingImpaired": "บกพร่องทางการได้ยิน" } From 663854bc1e079ed5c48bf06da60b7defe7cd6669 Mon Sep 17 00:00:00 2001 From: Patrick Barron <18354464+barronpm@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:15:36 -0500 Subject: [PATCH 70/97] Update test dependencies (#9094) --- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 8 ++++---- tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj | 4 ++-- .../Jellyfin.Controller.Tests.csproj | 6 +++--- tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj | 6 +++--- .../Jellyfin.Extensions.Tests.csproj | 6 +++--- .../Jellyfin.MediaEncoding.Hls.Tests.csproj | 4 ++-- .../Jellyfin.MediaEncoding.Keyframes.Tests.csproj | 4 ++-- .../Jellyfin.MediaEncoding.Tests.csproj | 6 +++--- tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj | 6 +++--- tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj | 6 +++--- .../Jellyfin.Networking.Tests.csproj | 6 +++--- .../Jellyfin.Providers.Tests.csproj | 6 +++--- .../Jellyfin.Server.Implementations.Tests.csproj | 6 +++--- .../Jellyfin.Server.Integration.Tests.csproj | 8 ++++---- tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj | 8 ++++---- .../Jellyfin.XbmcMetadata.Tests.csproj | 6 +++--- 16 files changed, 48 insertions(+), 48 deletions(-) diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index bd412bc769..c0e0d2b6b3 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -15,16 +15,16 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 3ca761b3d0..c74127f044 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -12,13 +12,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 650973c6ae..1ddf5139c9 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -12,14 +12,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj index cba9468001..dc4b58fecf 100644 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj index 075bcaac8e..16b18cc85e 100644 --- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj +++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj @@ -7,13 +7,13 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj index f7163edc7e..c20f3dd999 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj @@ -7,13 +7,13 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj index 72bfb3fd22..5cfad93a6a 100644 --- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj @@ -8,13 +8,13 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index e68e7f39aa..f824b6f3b2 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -21,9 +21,9 @@ - - - + + + all diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index 2c7e393af8..b6578a7f11 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -7,14 +7,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 0d9acf0e10..f10f9159dd 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -12,14 +12,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index 9e13dd4ad6..3a39daa364 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -12,15 +12,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj index 2d8e3c8f2f..6cc998d274 100644 --- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -13,14 +13,14 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index d91b4f00bc..82628d7339 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -21,15 +21,15 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index ecc3ebb86a..006b38a11d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -9,17 +9,17 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index 0ce2721c76..771fad6357 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -10,16 +10,16 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj index bde34d6394..0d69c3f611 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj +++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj @@ -13,14 +13,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 56ef45ebf004cd444066335084111781b983e853 Mon Sep 17 00:00:00 2001 From: Teo Baranga Date: Sat, 14 Jan 2023 20:15:50 +0000 Subject: [PATCH 71/97] Fix client supported image formats (#9071) --- Jellyfin.Api/Controllers/ImageController.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 534667c8c6..f866655c0b 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -28,7 +28,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Controllers @@ -2038,13 +2037,8 @@ namespace Jellyfin.Api.Controllers } var acceptParam = Request.Query[HeaderNames.Accept]; - if (StringValues.IsNullOrEmpty(acceptParam)) - { - return Array.Empty(); - } - // Can't be null, checked above - var supportsWebP = SupportsFormat(supportedFormats, acceptParam!, ImageFormat.Webp, false); + var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false); if (!supportsWebP) { @@ -2066,8 +2060,7 @@ namespace Jellyfin.Api.Controllers formats.Add(ImageFormat.Jpg); formats.Add(ImageFormat.Png); - // Can't be null, checked above - if (SupportsFormat(supportedFormats, acceptParam!, ImageFormat.Gif, true)) + if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true)) { formats.Add(ImageFormat.Gif); } @@ -2075,7 +2068,7 @@ namespace Jellyfin.Api.Controllers return formats.ToArray(); } - private bool SupportsFormat(IReadOnlyCollection requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll) + private bool SupportsFormat(IReadOnlyCollection requestAcceptTypes, string? acceptParam, ImageFormat format, bool acceptAll) { if (requestAcceptTypes.Contains(format.GetMimeType())) { From ab918c6292bb27a0e39f3d54f0f449b97246e59b Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 9 Jan 2023 00:07:53 +0100 Subject: [PATCH 72/97] Fine tune DB settings --- .../Data/BaseSqliteRepository.cs | 12 +++++++++++- .../Data/SqliteItemRepository.cs | 2 -- Jellyfin.Server/Program.cs | 4 ---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index ff9aa4c2a3..230f633a7c 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -60,11 +60,16 @@ namespace Emby.Server.Implementations.Data /// The cache size or null. protected virtual int? CacheSize => null; + /// + /// Gets the locking mode. . + /// + protected virtual string LockingMode => "EXCLUSIVE"; + /// /// Gets the journal mode. . /// /// The journal mode. - protected virtual string JournalMode => "TRUNCATE"; + protected virtual string JournalMode => "WAL"; /// /// Gets the page size. @@ -116,6 +121,11 @@ namespace Emby.Server.Implementations.Data WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); } + if (!string.IsNullOrWhiteSpace(LockingMode)) + { + WriteConnection.Execute("PRAGMA locking_mode=" + LockingMode); + } + if (!string.IsNullOrWhiteSpace(JournalMode)) { WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 763ff77f10..fb6902c398 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -359,8 +359,6 @@ namespace Emby.Server.Implementations.Data string[] queries = { - "PRAGMA locking_mode=EXCLUSIVE", - "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 1506530f0c..540375dce6 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -273,10 +273,6 @@ namespace Jellyfin.Server ServicePointManager.Expect100Continue = false; Batteries_V2.Init(); - if (raw.sqlite3_enable_shared_cache(1) != raw.SQLITE_OK) - { - _logger.LogWarning("Failed to enable shared cache for SQLite"); - } } /// From 73740f6c6ea8e75ed57ab27228e3796938a93c81 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 9 Jan 2023 14:14:19 +0100 Subject: [PATCH 73/97] Change synchronous_mode to normal --- Emby.Server.Implementations/Data/BaseSqliteRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 230f633a7c..acbccc6655 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Data /// /// The synchronous mode or null. /// - protected virtual SynchronousMode? Synchronous => null; + protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal; /// /// Gets or sets the write lock. From 6a8d24d9e926bd7b8f54132bf769f92790d9d68a Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 10 Jan 2023 22:29:05 +0100 Subject: [PATCH 74/97] Set journal_size_limit --- .../Data/BaseSqliteRepository.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index acbccc6655..1d61667f86 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -71,6 +71,12 @@ namespace Emby.Server.Implementations.Data /// The journal mode. protected virtual string JournalMode => "WAL"; + /// + /// Gets the journal size limit. . + /// + /// The journal size limit. + protected virtual int? JournalSizeLimit => 0; + /// /// Gets the page size. /// @@ -131,6 +137,11 @@ namespace Emby.Server.Implementations.Data WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode); } + if (JournalSizeLimit.HasValue) + { + WriteConnection.Execute("PRAGMA journal_size_limit=" + (int)JournalSizeLimit.Value); + } + if (Synchronous.HasValue) { WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); From 8ff0cb1e9dc0ac53ed099e58efa5366c377e393a Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 14 Jan 2023 21:39:42 +0100 Subject: [PATCH 75/97] Remove AddPeopleQueryIndex migration --- .../Data/SqliteItemRepository.cs | 36 ++------------ Jellyfin.Server/Migrations/MigrationRunner.cs | 1 - .../Routines/AddPeopleQueryIndex.cs | 49 ------------------- 3 files changed, 3 insertions(+), 83 deletions(-) delete mode 100644 Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index fb6902c398..bc703fe90d 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -383,39 +383,6 @@ namespace Emby.Server.Implementations.Data string[] postQueries = { - // obsolete - "drop index if exists idx_TypedBaseItems", - "drop index if exists idx_mediastreams", - "drop index if exists idx_mediastreams1", - "drop index if exists idx_" + ChaptersTableName, - "drop index if exists idx_UserDataKeys1", - "drop index if exists idx_UserDataKeys2", - "drop index if exists idx_TypeTopParentId3", - "drop index if exists idx_TypeTopParentId2", - "drop index if exists idx_TypeTopParentId4", - "drop index if exists idx_Type", - "drop index if exists idx_TypeTopParentId", - "drop index if exists idx_GuidType", - "drop index if exists idx_TopParentId", - "drop index if exists idx_TypeTopParentId6", - "drop index if exists idx_ItemValues2", - "drop index if exists Idx_ProviderIds", - "drop index if exists idx_ItemValues3", - "drop index if exists idx_ItemValues4", - "drop index if exists idx_ItemValues5", - "drop index if exists idx_UserDataKeys3", - "drop table if exists UserDataKeys", - "drop table if exists ProviderIds", - "drop index if exists Idx_ProviderIds1", - "drop table if exists Images", - "drop index if exists idx_Images", - "drop index if exists idx_TypeSeriesPresentationUniqueKey", - "drop index if exists idx_SeriesPresentationUniqueKey", - "drop index if exists idx_TypeSeriesPresentationUniqueKey2", - "drop index if exists idx_AncestorIds3", - "drop index if exists idx_AncestorIds4", - "drop index if exists idx_AncestorIds2", - "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", @@ -456,6 +423,9 @@ namespace Emby.Server.Implementations.Data // Used to update inherited tags "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", + + "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)", + "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)" }; using (var connection = GetConnection()) diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index e9a45c140f..23fb9e3708 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -38,7 +38,6 @@ namespace Jellyfin.Server.Migrations typeof(Routines.ReaddDefaultPluginRepository), typeof(Routines.MigrateDisplayPreferencesDb), typeof(Routines.RemoveDownloadImagesInAdvance), - typeof(Routines.AddPeopleQueryIndex), typeof(Routines.MigrateAuthenticationDb) }; diff --git a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs b/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs deleted file mode 100644 index 6343c422d5..0000000000 --- a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.IO; -using MediaBrowser.Controller; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Jellyfin.Server.Migrations.Routines -{ - /// - /// Migration to add table indexes to optimize the Persons query. - /// - public class AddPeopleQueryIndex : IMigrationRoutine - { - private const string DbFilename = "library.db"; - private readonly ILogger _logger; - private readonly IServerApplicationPaths _serverApplicationPaths; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public AddPeopleQueryIndex(ILogger logger, IServerApplicationPaths serverApplicationPaths) - { - _logger = logger; - _serverApplicationPaths = serverApplicationPaths; - } - - /// - public Guid Id => new Guid("DE009B59-BAAE-428D-A810-F67762DC05B8"); - - /// - public string Name => "AddPeopleQueryIndex"; - - /// - public bool PerformOnNewInstall => true; - - /// - public void Perform() - { - var databasePath = Path.Join(_serverApplicationPaths.DataPath, DbFilename); - using var connection = SQLite3.Open(databasePath, ConnectionFlags.ReadWrite, null); - _logger.LogInformation("Creating index idx_TypedBaseItemsUserDataKeyType"); - connection.Execute("CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);"); - _logger.LogInformation("Creating index idx_PeopleNameListOrder"); - connection.Execute("CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder);"); - } - } -} From 74a07f6d1c52533aa648da27cb3924d6bb41bb19 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 13 Jan 2023 21:37:37 -0500 Subject: [PATCH 76/97] Move Middleware to Jellyfin.Api --- .../Middleware/BaseUrlRedirectionMiddleware.cs | 2 +- .../Middleware/ExceptionMiddleware.cs | 2 +- .../Middleware/IpBasedAccessValidationMiddleware.cs | 2 +- .../Middleware/LanFilteringMiddleware.cs | 2 +- .../Middleware/LegacyEmbyRouteRewriteMiddleware.cs | 2 +- .../Middleware/QueryStringDecodingMiddleware.cs | 2 +- .../Middleware/ResponseTimeMiddleware.cs | 2 +- .../Middleware/RobotsRedirectionMiddleware.cs | 2 +- .../Middleware/ServerStartupMessageMiddleware.cs | 2 +- .../Middleware/UrlDecodeQueryFeature.cs | 2 +- .../Middleware/WebSocketHandlerMiddleware.cs | 2 +- Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 2 +- Jellyfin.Server/Startup.cs | 2 +- tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/BaseUrlRedirectionMiddleware.cs (99%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/ExceptionMiddleware.cs (99%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/IpBasedAccessValidationMiddleware.cs (97%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/LanFilteringMiddleware.cs (97%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/LegacyEmbyRouteRewriteMiddleware.cs (98%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/QueryStringDecodingMiddleware.cs (97%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/ResponseTimeMiddleware.cs (98%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/RobotsRedirectionMiddleware.cs (97%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/ServerStartupMessageMiddleware.cs (98%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/UrlDecodeQueryFeature.cs (98%) rename {Jellyfin.Server => Jellyfin.Api}/Middleware/WebSocketHandlerMiddleware.cs (97%) diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs similarity index 99% rename from Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs rename to Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs index 6ee5bf38a4..6bd9e0b084 100644 --- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Redirect requests without baseurl prefix to the baseurl prefixed URL. diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs similarity index 99% rename from Jellyfin.Server/Middleware/ExceptionMiddleware.cs rename to Jellyfin.Api/Middleware/ExceptionMiddleware.cs index 91dbce19a4..6b3aeb187a 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Exception Middleware. diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs rename to Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs index 0afcd61a05..f7af91e489 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs @@ -4,7 +4,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Validates the IP of requests coming from local networks wrt. remote access. diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/LanFilteringMiddleware.cs rename to Jellyfin.Api/Middleware/LanFilteringMiddleware.cs index 67bf24d2a5..18f13bbced 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs @@ -5,7 +5,7 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Validates the LAN host IP based on application configuration. diff --git a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs similarity index 98% rename from Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs rename to Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs index b214299df3..b73923c1e5 100644 --- a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs +++ b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Removes /emby and /mediabrowser from requested route. diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs rename to Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs index 24807ce383..4b6304e0e7 100644 --- a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs +++ b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// URL decodes the querystring before binding. diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs similarity index 98% rename from Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs rename to Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs index 531897cd49..3701d0f451 100644 --- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs +++ b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Response time middleware. diff --git a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs rename to Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs index fabcd2da7e..2e69580bee 100644 --- a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Redirect requests to robots.txt to web/robots.txt. diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs similarity index 98% rename from Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs rename to Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs index 2ec0633924..dcd64401a4 100644 --- a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs +++ b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller; using MediaBrowser.Model.Globalization; using Microsoft.AspNetCore.Http; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Shows a custom message during server startup. diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs similarity index 98% rename from Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs rename to Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs index 2f1d791573..d35e0fcfd9 100644 --- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs +++ b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Defines the . diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs similarity index 97% rename from Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs rename to Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs index b7a5d2b346..2cf1e5e4aa 100644 --- a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs +++ b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Api.Middleware { /// /// Handles WebSocket requests. diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index e291677475..463ca7321d 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using Jellyfin.Api.Middleware; using Jellyfin.Networking.Configuration; -using Jellyfin.Server.Middleware; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.OpenApi.Models; diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index f89f81c766..0062b8c056 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; using System.Text; +using Jellyfin.Api.Middleware; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; @@ -12,7 +13,6 @@ using Jellyfin.Server.HealthChecks; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Infrastructure; -using Jellyfin.Server.Middleware; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs index d15c9d6f54..797fc8f64b 100644 --- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs +++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Jellyfin.Server.Middleware; +using Jellyfin.Api.Middleware; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; From 7186b343bd21fe6b4e771b8530bd780a98a84472 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 13 Jan 2023 22:23:22 -0500 Subject: [PATCH 77/97] Move Formatters to Jellyfin.Api --- .../Formatters/CamelCaseJsonProfileFormatter.cs | 2 +- .../Formatters/CssOutputFormatter.cs | 2 +- .../Formatters/PascalCaseJsonProfileFormatter.cs | 2 +- .../Formatters/XmlOutputFormatter.cs | 2 +- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename {Jellyfin.Server => Jellyfin.Api}/Formatters/CamelCaseJsonProfileFormatter.cs (94%) rename {Jellyfin.Server => Jellyfin.Api}/Formatters/CssOutputFormatter.cs (97%) rename {Jellyfin.Server => Jellyfin.Api}/Formatters/PascalCaseJsonProfileFormatter.cs (95%) rename {Jellyfin.Server => Jellyfin.Api}/Formatters/XmlOutputFormatter.cs (96%) diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs similarity index 94% rename from Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs rename to Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs index ea8c5ecdb1..8f1f5dd940 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs @@ -2,7 +2,7 @@ using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; -namespace Jellyfin.Server.Formatters +namespace Jellyfin.Api.Formatters { /// /// Camel Case Json Profile Formatter. diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Api/Formatters/CssOutputFormatter.cs similarity index 97% rename from Jellyfin.Server/Formatters/CssOutputFormatter.cs rename to Jellyfin.Api/Formatters/CssOutputFormatter.cs index fdaa48f847..e88c8ad1b2 100644 --- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs +++ b/Jellyfin.Api/Formatters/CssOutputFormatter.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; -namespace Jellyfin.Server.Formatters +namespace Jellyfin.Api.Formatters { /// /// Css output formatter. diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs similarity index 95% rename from Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs rename to Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs index 03ca7dda72..5d77dbf4cc 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs @@ -3,7 +3,7 @@ using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; -namespace Jellyfin.Server.Formatters +namespace Jellyfin.Api.Formatters { /// /// Pascal Case Json Profile Formatter. diff --git a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs similarity index 96% rename from Jellyfin.Server/Formatters/XmlOutputFormatter.cs rename to Jellyfin.Api/Formatters/XmlOutputFormatter.cs index 156368d695..df8b1650be 100644 --- a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs +++ b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; -namespace Jellyfin.Server.Formatters +namespace Jellyfin.Api.Formatters { /// /// Xml output formatter. diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index f74152405a..e9af1cf83c 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -20,13 +20,13 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Auth.SyncPlayAccessPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Api.Formatters; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; -using Jellyfin.Server.Formatters; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; From 029d53502fafe11a67bde33551f1ab8b382829d7 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sat, 14 Jan 2023 15:47:26 -0500 Subject: [PATCH 78/97] Move some startup methods to StartupHelpers --- Jellyfin.Server/Helpers/StartupHelpers.cs | 276 ++++++++++++++++++ Jellyfin.Server/Program.cs | 268 +---------------- .../JellyfinApplicationFactory.cs | 5 +- 3 files changed, 284 insertions(+), 265 deletions(-) create mode 100644 Jellyfin.Server/Helpers/StartupHelpers.cs diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs new file mode 100644 index 0000000000..b10e34898c --- /dev/null +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -0,0 +1,276 @@ +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Emby.Server.Implementations; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Configuration; +using Serilog; +using SQLitePCL; + +namespace Jellyfin.Server.Helpers; + +/// +/// A class containing helper methods for server startup. +/// +public static class StartupHelpers +{ + /// + /// Create the data, config and log paths from the variety of inputs(command line args, + /// environment variables) or decide on what default to use. For Windows it's %AppPath% + /// for everything else the + /// XDG approach + /// is followed. + /// + /// The for this instance. + /// . + public static ServerApplicationPaths CreateApplicationPaths(StartupOptions options) + { + // dataDir + // IF --datadir + // ELSE IF $JELLYFIN_DATA_DIR + // ELSE IF windows, use <%APPDATA%>/jellyfin + // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin + // ELSE use $HOME/.local/share/jellyfin + var dataDir = options.DataDir; + if (string.IsNullOrEmpty(dataDir)) + { + dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR"); + + if (string.IsNullOrEmpty(dataDir)) + { + // LocalApplicationData follows the XDG spec on unix machines + dataDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "jellyfin"); + } + } + + // configDir + // IF --configdir + // ELSE IF $JELLYFIN_CONFIG_DIR + // ELSE IF --datadir, use /config (assume portable run) + // ELSE IF /config exists, use that + // ELSE IF windows, use /config + // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin + // ELSE $HOME/.config/jellyfin + var configDir = options.ConfigDir; + if (string.IsNullOrEmpty(configDir)) + { + configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR"); + + if (string.IsNullOrEmpty(configDir)) + { + if (options.DataDir is not null + || Directory.Exists(Path.Combine(dataDir, "config")) + || OperatingSystem.IsWindows()) + { + // Hang config folder off already set dataDir + configDir = Path.Combine(dataDir, "config"); + } + else + { + // $XDG_CONFIG_HOME defines the base directory relative to which + // user specific configuration files should be stored. + configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); + + // If $XDG_CONFIG_HOME is either not set or empty, + // a default equal to $HOME /.config should be used. + if (string.IsNullOrEmpty(configDir)) + { + configDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config"); + } + + configDir = Path.Combine(configDir, "jellyfin"); + } + } + } + + // cacheDir + // IF --cachedir + // ELSE IF $JELLYFIN_CACHE_DIR + // ELSE IF windows, use /cache + // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin + // ELSE HOME/.cache/jellyfin + var cacheDir = options.CacheDir; + if (string.IsNullOrEmpty(cacheDir)) + { + cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR"); + + if (string.IsNullOrEmpty(cacheDir)) + { + if (OperatingSystem.IsWindows()) + { + // Hang cache folder off already set dataDir + cacheDir = Path.Combine(dataDir, "cache"); + } + else + { + // $XDG_CACHE_HOME defines the base directory relative to which + // user specific non-essential data files should be stored. + cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); + + // If $XDG_CACHE_HOME is either not set or empty, + // a default equal to $HOME/.cache should be used. + if (string.IsNullOrEmpty(cacheDir)) + { + cacheDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cache"); + } + + cacheDir = Path.Combine(cacheDir, "jellyfin"); + } + } + } + + // webDir + // IF --webdir + // ELSE IF $JELLYFIN_WEB_DIR + // ELSE /jellyfin-web + var webDir = options.WebDir; + if (string.IsNullOrEmpty(webDir)) + { + webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); + + if (string.IsNullOrEmpty(webDir)) + { + // Use default location under ResourcesPath + webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web"); + } + } + + // logDir + // IF --logdir + // ELSE IF $JELLYFIN_LOG_DIR + // ELSE IF --datadir, use /log (assume portable run) + // ELSE /log + var logDir = options.LogDir; + if (string.IsNullOrEmpty(logDir)) + { + logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR"); + + if (string.IsNullOrEmpty(logDir)) + { + // Hang log folder off already set dataDir + logDir = Path.Combine(dataDir, "log"); + } + } + + // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162 + dataDir = Path.GetFullPath(dataDir); + logDir = Path.GetFullPath(logDir); + configDir = Path.GetFullPath(configDir); + cacheDir = Path.GetFullPath(cacheDir); + webDir = Path.GetFullPath(webDir); + + // Ensure the main folders exist before we continue + try + { + Directory.CreateDirectory(dataDir); + Directory.CreateDirectory(logDir); + Directory.CreateDirectory(configDir); + Directory.CreateDirectory(cacheDir); + } + catch (IOException ex) + { + Console.Error.WriteLine("Error whilst attempting to create folder"); + Console.Error.WriteLine(ex.ToString()); + Environment.Exit(1); + } + + return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); + } + + /// + /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist + /// already. + /// + /// The application paths. + /// A task representing the creation of the configuration file, or a completed task if the file already exists. + public static async Task InitLoggingConfigFile(IApplicationPaths appPaths) + { + // Do nothing if the config file already exists + string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, Program.LoggingConfigFileDefault); + if (File.Exists(configPath)) + { + return; + } + + // Get a stream of the resource contents + // NOTE: The .csproj name is used instead of the assembly name in the resource path + const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; + Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) + ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); + await using (resource.ConfigureAwait(false)) + { + Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await using (dst.ConfigureAwait(false)) + { + // Copy the resource contents to the expected file path for the config file + await resource.CopyToAsync(dst).ConfigureAwait(false); + } + } + } + + /// + /// Initialize Serilog using configuration and fall back to defaults on failure. + /// + /// The configuration object. + /// The application paths. + public static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths) + { + try + { + // Serilog.Log is used by SerilogLoggerFactory when no logger is specified + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .CreateLogger(); + } + catch (Exception ex) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}", + formatProvider: CultureInfo.InvariantCulture) + .WriteTo.Async(x => x.File( + Path.Combine(appPaths.LogDirectoryPath, "log_.log"), + rollingInterval: RollingInterval.Day, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", + formatProvider: CultureInfo.InvariantCulture, + encoding: Encoding.UTF8)) + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .CreateLogger(); + + Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); + } + } + + /// + /// Call static initialization methods for the application. + /// + public static void PerformStaticInitialization() + { + // Make sure we have all the code pages we can get + // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // Increase the max http request limit + // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. + ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); + + // Disable the "Expect: 100-Continue" header by default + // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c + ServicePointManager.Expect100Continue = false; + + Batteries_V2.Init(); + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 540375dce6..f6fa0ff5ba 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -1,21 +1,19 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Runtime.Versioning; -using System.Text; using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; +using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; -using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -25,7 +23,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; -using SQLitePCL; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -99,7 +96,7 @@ namespace Jellyfin.Server Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject.ToString()); AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole; - ServerApplicationPaths appPaths = CreateApplicationPaths(options); + ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -108,13 +105,12 @@ namespace Jellyfin.Server Environment.SetEnvironmentVariable("NEOReadDebugKeys", "1"); Environment.SetEnvironmentVariable("EnableExtendedVaFormats", "1"); - await InitLoggingConfigFile(appPaths).ConfigureAwait(false); + await StartupHelpers.InitLoggingConfigFile(appPaths).ConfigureAwait(false); // Create an instance of the application configuration to use for application startup IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); - // Initialize logging framework - InitializeLoggingFramework(startupConfig, appPaths); + StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); // Log uncaught exceptions to the logging instead of std error @@ -173,7 +169,7 @@ namespace Jellyfin.Server } } - PerformStaticInitialization(); + StartupHelpers.PerformStaticInitialization(); Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory); var appHost = new CoreAppHost( @@ -255,26 +251,6 @@ namespace Jellyfin.Server } } - /// - /// Call static initialization methods for the application. - /// - public static void PerformStaticInitialization() - { - // Make sure we have all the code pages we can get - // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - - // Increase the max http request limit - // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. - ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); - - // Disable the "Expect: 100-Continue" header by default - // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c - ServicePointManager.Expect100Continue = false; - - Batteries_V2.Init(); - } - /// /// Configure the web host builder. /// @@ -344,206 +320,6 @@ namespace Jellyfin.Server .UseStartup(_ => new Startup(appHost)); } - /// - /// Create the data, config and log paths from the variety of inputs(command line args, - /// environment variables) or decide on what default to use. For Windows it's %AppPath% - /// for everything else the - /// XDG approach - /// is followed. - /// - /// The for this instance. - /// . - private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options) - { - // dataDir - // IF --datadir - // ELSE IF $JELLYFIN_DATA_DIR - // ELSE IF windows, use <%APPDATA%>/jellyfin - // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin - // ELSE use $HOME/.local/share/jellyfin - var dataDir = options.DataDir; - if (string.IsNullOrEmpty(dataDir)) - { - dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR"); - - if (string.IsNullOrEmpty(dataDir)) - { - // LocalApplicationData follows the XDG spec on unix machines - dataDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "jellyfin"); - } - } - - // configDir - // IF --configdir - // ELSE IF $JELLYFIN_CONFIG_DIR - // ELSE IF --datadir, use /config (assume portable run) - // ELSE IF /config exists, use that - // ELSE IF windows, use /config - // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin - // ELSE $HOME/.config/jellyfin - var configDir = options.ConfigDir; - if (string.IsNullOrEmpty(configDir)) - { - configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR"); - - if (string.IsNullOrEmpty(configDir)) - { - if (options.DataDir is not null - || Directory.Exists(Path.Combine(dataDir, "config")) - || OperatingSystem.IsWindows()) - { - // Hang config folder off already set dataDir - configDir = Path.Combine(dataDir, "config"); - } - else - { - // $XDG_CONFIG_HOME defines the base directory relative to which - // user specific configuration files should be stored. - configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); - - // If $XDG_CONFIG_HOME is either not set or empty, - // a default equal to $HOME /.config should be used. - if (string.IsNullOrEmpty(configDir)) - { - configDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".config"); - } - - configDir = Path.Combine(configDir, "jellyfin"); - } - } - } - - // cacheDir - // IF --cachedir - // ELSE IF $JELLYFIN_CACHE_DIR - // ELSE IF windows, use /cache - // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin - // ELSE HOME/.cache/jellyfin - var cacheDir = options.CacheDir; - if (string.IsNullOrEmpty(cacheDir)) - { - cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR"); - - if (string.IsNullOrEmpty(cacheDir)) - { - if (OperatingSystem.IsWindows()) - { - // Hang cache folder off already set dataDir - cacheDir = Path.Combine(dataDir, "cache"); - } - else - { - // $XDG_CACHE_HOME defines the base directory relative to which - // user specific non-essential data files should be stored. - cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); - - // If $XDG_CACHE_HOME is either not set or empty, - // a default equal to $HOME/.cache should be used. - if (string.IsNullOrEmpty(cacheDir)) - { - cacheDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".cache"); - } - - cacheDir = Path.Combine(cacheDir, "jellyfin"); - } - } - } - - // webDir - // IF --webdir - // ELSE IF $JELLYFIN_WEB_DIR - // ELSE /jellyfin-web - var webDir = options.WebDir; - if (string.IsNullOrEmpty(webDir)) - { - webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); - - if (string.IsNullOrEmpty(webDir)) - { - // Use default location under ResourcesPath - webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web"); - } - } - - // logDir - // IF --logdir - // ELSE IF $JELLYFIN_LOG_DIR - // ELSE IF --datadir, use /log (assume portable run) - // ELSE /log - var logDir = options.LogDir; - if (string.IsNullOrEmpty(logDir)) - { - logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR"); - - if (string.IsNullOrEmpty(logDir)) - { - // Hang log folder off already set dataDir - logDir = Path.Combine(dataDir, "log"); - } - } - - // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162 - dataDir = Path.GetFullPath(dataDir); - logDir = Path.GetFullPath(logDir); - configDir = Path.GetFullPath(configDir); - cacheDir = Path.GetFullPath(cacheDir); - webDir = Path.GetFullPath(webDir); - - // Ensure the main folders exist before we continue - try - { - Directory.CreateDirectory(dataDir); - Directory.CreateDirectory(logDir); - Directory.CreateDirectory(configDir); - Directory.CreateDirectory(cacheDir); - } - catch (IOException ex) - { - Console.Error.WriteLine("Error whilst attempting to create folder"); - Console.Error.WriteLine(ex.ToString()); - Environment.Exit(1); - } - - return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); - } - - /// - /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist - /// already. - /// - /// The application paths. - /// A task representing the creation of the configuration file, or a completed task if the file already exists. - public static async Task InitLoggingConfigFile(IApplicationPaths appPaths) - { - // Do nothing if the config file already exists - string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault); - if (File.Exists(configPath)) - { - return; - } - - // Get a stream of the resource contents - // NOTE: The .csproj name is used instead of the assembly name in the resource path - const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; - Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) - ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); - await using (resource.ConfigureAwait(false)) - { - Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await using (dst.ConfigureAwait(false)) - { - // Copy the resource contents to the expected file path for the config file - await resource.CopyToAsync(dst).ConfigureAwait(false); - } - } - } - /// /// Create the application configuration. /// @@ -579,40 +355,6 @@ namespace Jellyfin.Server .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); } - /// - /// Initialize Serilog using configuration and fall back to defaults on failure. - /// - private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths) - { - try - { - // Serilog.Log is used by SerilogLoggerFactory when no logger is specified - Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .Enrich.FromLogContext() - .Enrich.WithThreadId() - .CreateLogger(); - } - catch (Exception ex) - { - Log.Logger = new LoggerConfiguration() - .WriteTo.Console( - outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}", - formatProvider: CultureInfo.InvariantCulture) - .WriteTo.Async(x => x.File( - Path.Combine(appPaths.LogDirectoryPath, "log_.log"), - rollingInterval: RollingInterval.Day, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", - formatProvider: CultureInfo.InvariantCulture, - encoding: Encoding.UTF8)) - .Enrich.FromLogContext() - .Enrich.WithThreadId() - .CreateLogger(); - - Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); - } - } - private static void StartNewInstance(StartupOptions options) { _logger.LogInformation("Starting new instance"); diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 1bfa5996d8..3faea64bec 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Threading; using Emby.Server.Implementations; +using Jellyfin.Server.Helpers; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; @@ -33,7 +34,7 @@ namespace Jellyfin.Server.Integration.Tests Log.Logger = new LoggerConfiguration() .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) .CreateLogger(); - Program.PerformStaticInitialization(); + StartupHelpers.PerformStaticInitialization(); } /// @@ -63,7 +64,7 @@ namespace Jellyfin.Server.Integration.Tests // Create the logging config file // TODO: We shouldn't need to do this since we are only logging to console - Program.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult(); + StartupHelpers.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult(); // Create a copy of the application configuration to use for startup var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths); From f8ca71ee157079aeee075f92f9537e9d580d584d Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sun, 15 Jan 2023 11:46:30 -0500 Subject: [PATCH 79/97] Move WebHostBuilder extension method to separate file --- .../Extensions/WebHostBuilderExtensions.cs | 90 ++++++++++++++ Jellyfin.Server/Helpers/StartupHelpers.cs | 50 ++++++++ Jellyfin.Server/Program.cs | 114 +----------------- .../JellyfinApplicationFactory.cs | 4 +- 4 files changed, 147 insertions(+), 111 deletions(-) create mode 100644 Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs new file mode 100644 index 0000000000..58d3e1b2d9 --- /dev/null +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +using System.Net; +using Jellyfin.Server.Helpers; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Extensions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Extensions; + +/// +/// Extensions for configuring the web host builder. +/// +public static class WebHostBuilderExtensions +{ + /// + /// Configure the web host builder. + /// + /// The builder to configure. + /// The application host. + /// The application configuration. + /// The application paths. + /// The logger. + /// The configured web host builder. + public static IWebHostBuilder ConfigureWebHostBuilder( + this IWebHostBuilder builder, + CoreAppHost appHost, + IConfiguration startupConfig, + IApplicationPaths appPaths, + ILogger logger) + { + return builder + .UseKestrel((builderContext, options) => + { + var addresses = appHost.NetManager.GetAllBindInterfaces(); + + bool flagged = false; + foreach (IPObject netAdd in addresses) + { + logger.LogInformation("Kestrel listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All Addresses" : netAdd); + options.Listen(netAdd.Address, appHost.HttpPort); + if (appHost.ListenWithHttps) + { + options.Listen( + netAdd.Address, + appHost.HttpsPort, + listenOptions => listenOptions.UseHttps(appHost.Certificate)); + } + else if (builderContext.HostingEnvironment.IsDevelopment()) + { + try + { + options.Listen( + netAdd.Address, + appHost.HttpsPort, + listenOptions => listenOptions.UseHttps()); + } + catch (InvalidOperationException) + { + if (!flagged) + { + logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted"); + flagged = true; + } + } + } + } + + // Bind to unix socket (only on unix systems) + if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) + { + var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); + + // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 + if (File.Exists(socketPath)) + { + File.Delete(socketPath); + } + + options.ListenUnixSocket(socketPath); + logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); + } + }) + .UseStartup(_ => new Startup(appHost)); + } +} diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs index b10e34898c..f1bb9b2831 100644 --- a/Jellyfin.Server/Helpers/StartupHelpers.cs +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -2,14 +2,18 @@ using System; using System.Globalization; using System.IO; using System.Net; +using System.Runtime.Versioning; using System.Text; using System.Threading.Tasks; using Emby.Server.Implementations; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.IO; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Serilog; using SQLitePCL; +using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server.Helpers; @@ -187,6 +191,52 @@ public static class StartupHelpers return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir); } + /// + /// Gets the path for the unix socket Kestrel should bind to. + /// + /// The startup config. + /// The application paths. + /// The path for Kestrel to bind to. + public static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths) + { + var socketPath = startupConfig.GetUnixSocketPath(); + + if (string.IsNullOrEmpty(socketPath)) + { + var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + var socketFile = "jellyfin.sock"; + if (xdgRuntimeDir is null) + { + // Fall back to config dir + socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, socketFile); + } + else + { + socketPath = Path.Join(xdgRuntimeDir, socketFile); + } + } + + return socketPath; + } + + /// + /// Sets the unix file permissions for Kestrel's socket file. + /// + /// The startup config. + /// The socket path. + /// The logger. + [UnsupportedOSPlatform("windows")] + public static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath, ILogger logger) + { + var socketPerms = startupConfig.GetUnixSocketPermissions(); + + if (!string.IsNullOrEmpty(socketPerms)) + { + File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8)); + logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms); + } + } + /// /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist /// already. diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index f6fa0ff5ba..b817ea6275 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -3,18 +3,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Net; using System.Reflection; -using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; +using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -182,7 +179,7 @@ namespace Jellyfin.Server { var host = Host.CreateDefaultBuilder() .ConfigureServices(services => appHost.Init(services)) - .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths)) + .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) .UseSerilog() .Build(); @@ -199,9 +196,9 @@ namespace Jellyfin.Server if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { - var socketPath = GetUnixSocketPath(startupConfig, appPaths); + var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); - SetUnixSocketPermissions(startupConfig, socketPath); + StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger); } } catch (Exception ex) when (ex is not TaskCanceledException) @@ -251,75 +248,6 @@ namespace Jellyfin.Server } } - /// - /// Configure the web host builder. - /// - /// The builder to configure. - /// The application host. - /// The application configuration. - /// The application paths. - /// The configured web host builder. - public static IWebHostBuilder ConfigureWebHostBuilder( - this IWebHostBuilder builder, - CoreAppHost appHost, - IConfiguration startupConfig, - IApplicationPaths appPaths) - { - return builder - .UseKestrel((builderContext, options) => - { - var addresses = appHost.NetManager.GetAllBindInterfaces(); - - bool flagged = false; - foreach (IPObject netAdd in addresses) - { - _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd); - options.Listen(netAdd.Address, appHost.HttpPort); - if (appHost.ListenWithHttps) - { - options.Listen( - netAdd.Address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); - } - else if (builderContext.HostingEnvironment.IsDevelopment()) - { - try - { - options.Listen( - netAdd.Address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps()); - } - catch (InvalidOperationException) - { - if (!flagged) - { - _logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); - flagged = true; - } - } - } - } - - // Bind to unix socket (only on unix systems) - if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) - { - var socketPath = GetUnixSocketPath(startupConfig, appPaths); - - // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 - if (File.Exists(socketPath)) - { - File.Delete(socketPath); - } - - options.ListenUnixSocket(socketPath); - _logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); - } - }) - .UseStartup(_ => new Startup(appHost)); - } - /// /// Create the application configuration. /// @@ -393,39 +321,5 @@ namespace Jellyfin.Server return "\"" + arg + "\""; } - - private static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths) - { - var socketPath = startupConfig.GetUnixSocketPath(); - - if (string.IsNullOrEmpty(socketPath)) - { - var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); - var socketFile = "jellyfin.sock"; - if (xdgRuntimeDir is null) - { - // Fall back to config dir - socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, socketFile); - } - else - { - socketPath = Path.Join(xdgRuntimeDir, socketFile); - } - } - - return socketPath; - } - - [UnsupportedOSPlatform("windows")] - private static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath) - { - var socketPerms = startupConfig.GetUnixSocketPermissions(); - - if (!string.IsNullOrEmpty(socketPerms)) - { - File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8)); - _logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms); - } - } } } diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 3faea64bec..55bc43455f 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Threading; using Emby.Server.Implementations; +using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; @@ -12,6 +13,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; @@ -82,7 +84,7 @@ namespace Jellyfin.Server.Integration.Tests _disposableComponents.Add(appHost); builder.ConfigureServices(services => appHost.Init(services)) - .ConfigureWebHostBuilder(appHost, startupConfig, appPaths) + .ConfigureWebHostBuilder(appHost, startupConfig, appPaths, NullLogger.Instance) .ConfigureAppConfiguration((context, builder) => { builder From dc85d86ea1db7608368eadbfee80f7618653f42d Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sun, 15 Jan 2023 15:39:57 -0500 Subject: [PATCH 80/97] Enable in-process restarting --- .../ApplicationHost.cs | 13 +-- .../IStartupOptions.cs | 10 --- .../Plugins/PluginManager.cs | 12 ++- Jellyfin.Server/Program.cs | 81 +++++++------------ Jellyfin.Server/StartupOptions.cs | 8 -- MediaBrowser.Common/IApplicationHost.cs | 6 -- MediaBrowser.Common/Plugins/IPluginManager.cs | 5 ++ 7 files changed, 46 insertions(+), 89 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7b3d07dfc1..7b40f530c9 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -193,11 +193,6 @@ namespace Emby.Server.Implementations /// private string PublishedServerUrl => _startupConfig[AddressOverrideKey]; - /// - /// Gets a value indicating whether this instance can self restart. - /// - public bool CanSelfRestart => _startupOptions.RestartPath is not null; - public bool CoreStartupHasCompleted { get; private set; } public virtual bool CanLaunchWebBrowser @@ -935,17 +930,13 @@ namespace Emby.Server.Implementations /// public void Restart() { - if (!CanSelfRestart) - { - throw new PlatformNotSupportedException("The server is unable to self-restart. Please restart manually."); - } - if (IsShuttingDown) { return; } IsShuttingDown = true; + _pluginManager.UnloadAssemblies(); Task.Run(async () => { @@ -1047,7 +1038,7 @@ namespace Emby.Server.Implementations CachePath = ApplicationPaths.CachePath, OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(), OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name, - CanSelfRestart = CanSelfRestart, + CanSelfRestart = true, CanLaunchWebBrowser = CanLaunchWebBrowser, TranscodingTempPath = ConfigurationManager.GetTranscodePath(), ServerName = FriendlyName, diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index 3769ae4dd8..b7bcaace1b 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -20,16 +20,6 @@ namespace Emby.Server.Implementations /// string? PackageName { get; } - /// - /// Gets the value of the --restartpath command line option. - /// - string? RestartPath { get; } - - /// - /// Gets the value of the --restartargs command line option. - /// - string? RestartArgs { get; } - /// /// Gets the value of the --published-server-url command line option. /// diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 14e7c22696..6ef66f2b5d 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; +using System.Runtime.Loader; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -30,6 +31,7 @@ namespace Emby.Server.Implementations.Plugins { private readonly string _pluginsPath; private readonly Version _appVersion; + private readonly AssemblyLoadContext _assemblyLoadContext; private readonly JsonSerializerOptions _jsonOptions; private readonly ILogger _logger; private readonly IApplicationHost _appHost; @@ -76,6 +78,8 @@ namespace Emby.Server.Implementations.Plugins _appHost = appHost; _minimumVersion = new Version(0, 0, 0, 1); _plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List(); + + _assemblyLoadContext = new AssemblyLoadContext("PluginContext", true); } private IHttpClientFactory HttpClientFactory @@ -124,7 +128,7 @@ namespace Emby.Server.Implementations.Plugins Assembly assembly; try { - assembly = Assembly.LoadFrom(file); + assembly = _assemblyLoadContext.LoadFromAssemblyPath(file); // Load all required types to verify that the plugin will load assembly.GetTypes(); @@ -156,6 +160,12 @@ namespace Emby.Server.Implementations.Plugins } } + /// + public void UnloadAssemblies() + { + _assemblyLoadContext.Unload(); + } + /// /// Creates all the plugin instances. /// diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index b817ea6275..dded20347b 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -12,6 +12,7 @@ using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -40,8 +41,9 @@ namespace Jellyfin.Server /// public const string LoggingConfigFileSystem = "logging.json"; - private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); + private static CancellationTokenSource _tokenSource = new(); + private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -86,11 +88,11 @@ namespace Jellyfin.Server private static async Task StartApp(StartupOptions options) { - var startTimestamp = Stopwatch.GetTimestamp(); + _startTimestamp = Stopwatch.GetTimestamp(); // Log all uncaught exceptions to std error static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) => - Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject.ToString()); + Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject); AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole; ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); @@ -151,14 +153,14 @@ namespace Jellyfin.Server // If hosting the web client, validate the client content path if (startupConfig.HostWebClient()) { - string? webContentPath = appPaths.WebPath; + var webContentPath = appPaths.WebPath; if (!Directory.Exists(webContentPath) || !Directory.EnumerateFiles(webContentPath).Any()) { _logger.LogError( "The server is expected to host the web client, but the provided content directory is either " + "invalid or empty: {WebContentPath}. If you do not want to host the web client with the " + "server, you may set the '--nowebclient' command line flag, or set" + - "'{ConfigKey}=false' in your config settings.", + "'{ConfigKey}=false' in your config settings", webContentPath, HostWebClientKey); Environment.ExitCode = 1; @@ -169,15 +171,31 @@ namespace Jellyfin.Server StartupHelpers.PerformStaticInitialization(); Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory); + do + { + _restartOnShutdown = false; + await StartServer(appPaths, options, startupConfig).ConfigureAwait(false); + + if (_restartOnShutdown) + { + _tokenSource = new CancellationTokenSource(); + _startTimestamp = Stopwatch.GetTimestamp(); + } + } while (_restartOnShutdown); + } + + private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) + { var appHost = new CoreAppHost( appPaths, _loggerFactory, options, startupConfig); + IHost? host = null; try { - var host = Host.CreateDefaultBuilder() + host = Host.CreateDefaultBuilder() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) @@ -203,13 +221,13 @@ namespace Jellyfin.Server } catch (Exception ex) when (ex is not TaskCanceledException) { - _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again."); + _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again"); throw; } await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false); - _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(startTimestamp)); + _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); // Block main thread until shutdown await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); @@ -220,7 +238,7 @@ namespace Jellyfin.Server } catch (Exception ex) { - _logger.LogCritical(ex, "Error while starting server."); + _logger.LogCritical(ex, "Error while starting server"); } finally { @@ -240,11 +258,7 @@ namespace Jellyfin.Server } await appHost.DisposeAsync().ConfigureAwait(false); - } - - if (_restartOnShutdown) - { - StartNewInstance(options); + host?.Dispose(); } } @@ -282,44 +296,5 @@ namespace Jellyfin.Server .AddEnvironmentVariables("JELLYFIN_") .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); } - - private static void StartNewInstance(StartupOptions options) - { - _logger.LogInformation("Starting new instance"); - - var module = options.RestartPath; - - if (string.IsNullOrWhiteSpace(module)) - { - module = Environment.GetCommandLineArgs()[0]; - } - - string commandLineArgsString; - if (options.RestartArgs is not null) - { - commandLineArgsString = options.RestartArgs; - } - else - { - commandLineArgsString = string.Join( - ' ', - Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument)); - } - - _logger.LogInformation("Executable: {0}", module); - _logger.LogInformation("Arguments: {0}", commandLineArgsString); - - Process.Start(module, commandLineArgsString); - } - - private static string NormalizeCommandLineArgument(string arg) - { - if (!arg.Contains(' ', StringComparison.Ordinal)) - { - return arg; - } - - return "\"" + arg + "\""; - } } } diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 0d9f379e0e..c3989751ca 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -63,14 +63,6 @@ namespace Jellyfin.Server [Option("package-name", Required = false, HelpText = "Used when packaging Jellyfin (example, synology).")] public string? PackageName { get; set; } - /// - [Option("restartpath", Required = false, HelpText = "Path to restart script.")] - public string? RestartPath { get; set; } - - /// - [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] - public string? RestartArgs { get; set; } - /// [Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")] public string? PublishedServerUrl { get; set; } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 53683cdbdf..96ee701b38 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -47,12 +47,6 @@ namespace MediaBrowser.Common /// true if this instance is shutting down; otherwise, false. bool IsShuttingDown { get; } - /// - /// Gets a value indicating whether this instance can self restart. - /// - /// true if this instance can self restart; otherwise, false. - bool CanSelfRestart { get; } - /// /// Gets the application version. /// diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs index 176bcbbd54..fa92d383a2 100644 --- a/MediaBrowser.Common/Plugins/IPluginManager.cs +++ b/MediaBrowser.Common/Plugins/IPluginManager.cs @@ -29,6 +29,11 @@ namespace MediaBrowser.Common.Plugins /// An IEnumerable{Assembly}. IEnumerable LoadAssemblies(); + /// + /// Unloads all of the assemblies. + /// + void UnloadAssemblies(); + /// /// Registers the plugin's services with the DI. /// Note: DI is not yet instantiated yet. From a48f18887468015876a8b056f15b68d6ef49ce04 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sun, 15 Jan 2023 17:00:38 -0500 Subject: [PATCH 81/97] Use separate assembly load contexts per plugin --- .../Plugins/PluginManager.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 6ef66f2b5d..3be20e7e3f 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.Plugins { private readonly string _pluginsPath; private readonly Version _appVersion; - private readonly AssemblyLoadContext _assemblyLoadContext; + private readonly List _assemblyLoadContexts; private readonly JsonSerializerOptions _jsonOptions; private readonly ILogger _logger; private readonly IApplicationHost _appHost; @@ -79,7 +79,7 @@ namespace Emby.Server.Implementations.Plugins _minimumVersion = new Version(0, 0, 0, 1); _plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List(); - _assemblyLoadContext = new AssemblyLoadContext("PluginContext", true); + _assemblyLoadContexts = new List(); } private IHttpClientFactory HttpClientFactory @@ -128,7 +128,10 @@ namespace Emby.Server.Implementations.Plugins Assembly assembly; try { - assembly = _assemblyLoadContext.LoadFromAssemblyPath(file); + var assemblyLoadContext = new AssemblyLoadContext($"{plugin.Name} ${plugin.Version}", true); + _assemblyLoadContexts.Add(assemblyLoadContext); + + assembly = assemblyLoadContext.LoadFromAssemblyPath(file); // Load all required types to verify that the plugin will load assembly.GetTypes(); @@ -163,7 +166,10 @@ namespace Emby.Server.Implementations.Plugins /// public void UnloadAssemblies() { - _assemblyLoadContext.Unload(); + foreach (var assemblyLoadContext in _assemblyLoadContexts) + { + assemblyLoadContext.Unload(); + } } /// From 577d396649f44a26f9f8bf291a58994d414e23a6 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Sun, 15 Jan 2023 17:35:36 -0500 Subject: [PATCH 82/97] Use custom plugin assembly load context --- .../Plugins/PluginLoadContext.cs | 33 +++++++++++++++++++ .../Plugins/PluginManager.cs | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 Emby.Server.Implementations/Plugins/PluginLoadContext.cs diff --git a/Emby.Server.Implementations/Plugins/PluginLoadContext.cs b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs new file mode 100644 index 0000000000..d04e9cf685 --- /dev/null +++ b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Emby.Server.Implementations.Plugins; + +/// +/// A custom for loading Jellyfin plugins. +/// +public class PluginLoadContext : AssemblyLoadContext +{ + private readonly AssemblyDependencyResolver _resolver; + + /// + /// Initializes a new instance of the class. + /// + /// The path of the plugin assembly. + public PluginLoadContext(string path) : base(true) + { + _resolver = new AssemblyDependencyResolver(path); + } + + /// + protected override Assembly? Load(AssemblyName assemblyName) + { + var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath is not null) + { + return LoadFromAssemblyPath(assemblyPath); + } + + return null; + } +} diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 3be20e7e3f..f2212f4dcb 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Plugins Assembly assembly; try { - var assemblyLoadContext = new AssemblyLoadContext($"{plugin.Name} ${plugin.Version}", true); + var assemblyLoadContext = new PluginLoadContext(file); _assemblyLoadContexts.Add(assemblyLoadContext); assembly = assemblyLoadContext.LoadFromAssemblyPath(file); From 921618368b389bb90a4f9bd1e87fcb31e88ce4f3 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 16 Jan 2023 10:05:36 -0500 Subject: [PATCH 83/97] Remove unused schema --- Jellyfin.Server.Implementations/JellyfinDb.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index dc4f53913c..e34e12e3fe 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -153,7 +153,6 @@ namespace Jellyfin.Server.Implementations { modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc); base.OnModelCreating(modelBuilder); - modelBuilder.HasDefaultSchema("jellyfin"); // Configuration for each entity is in it's own class inside 'ModelConfiguration'. modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDb).Assembly); From 2a86723caf271be73dca39c6dd3b5f11044eac28 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 16 Jan 2023 10:06:25 -0500 Subject: [PATCH 84/97] Use file-scoped namespace in db context --- Jellyfin.Server.Implementations/JellyfinDb.cs | 283 +++++++++--------- 1 file changed, 141 insertions(+), 142 deletions(-) diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index e34e12e3fe..6443100806 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -8,154 +8,153 @@ using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Interfaces; using Microsoft.EntityFrameworkCore; -namespace Jellyfin.Server.Implementations +namespace Jellyfin.Server.Implementations; + +/// +public class JellyfinDb : DbContext { - /// - public class JellyfinDb : DbContext + /// + /// Initializes a new instance of the class. + /// + /// The database context options. + public JellyfinDb(DbContextOptions options) : base(options) { - /// - /// Initializes a new instance of the class. - /// - /// The database context options. - public JellyfinDb(DbContextOptions options) : base(options) + } + + /// + /// Gets or sets the default connection string. + /// + public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db"; + + public virtual DbSet AccessSchedules { get; set; } + + public virtual DbSet ActivityLogs { get; set; } + + public virtual DbSet ApiKeys { get; set; } + + public virtual DbSet Devices { get; set; } + + public virtual DbSet DeviceOptions { get; set; } + + public virtual DbSet DisplayPreferences { get; set; } + + public virtual DbSet ImageInfos { get; set; } + + public virtual DbSet ItemDisplayPreferences { get; set; } + + public virtual DbSet CustomItemDisplayPreferences { get; set; } + + public virtual DbSet Permissions { get; set; } + + public virtual DbSet Preferences { get; set; } + + public virtual DbSet Users { get; set; } + + /*public virtual DbSet Artwork { get; set; } + + public virtual DbSet Books { get; set; } + + public virtual DbSet BookMetadata { get; set; } + + public virtual DbSet Chapters { get; set; } + + public virtual DbSet Collections { get; set; } + + public virtual DbSet CollectionItems { get; set; } + + public virtual DbSet Companies { get; set; } + + public virtual DbSet CompanyMetadata { get; set; } + + public virtual DbSet CustomItems { get; set; } + + public virtual DbSet CustomItemMetadata { get; set; } + + public virtual DbSet Episodes { get; set; } + + public virtual DbSet EpisodeMetadata { get; set; } + + public virtual DbSet Genres { get; set; } + + public virtual DbSet Groups { get; set; } + + public virtual DbSet Libraries { get; set; } + + public virtual DbSet LibraryItems { get; set; } + + public virtual DbSet LibraryRoot { get; set; } + + public virtual DbSet MediaFiles { get; set; } + + public virtual DbSet MediaFileStream { get; set; } + + public virtual DbSet Metadata { get; set; } + + public virtual DbSet MetadataProviders { get; set; } + + public virtual DbSet MetadataProviderIds { get; set; } + + public virtual DbSet Movies { get; set; } + + public virtual DbSet MovieMetadata { get; set; } + + public virtual DbSet MusicAlbums { get; set; } + + public virtual DbSet MusicAlbumMetadata { get; set; } + + public virtual DbSet People { get; set; } + + public virtual DbSet PersonRoles { get; set; } + + public virtual DbSet Photo { get; set; } + + public virtual DbSet PhotoMetadata { get; set; } + + public virtual DbSet ProviderMappings { get; set; } + + public virtual DbSet Ratings { get; set; } + + /// + /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to + /// store review ratings, not age ratings. + /// + public virtual DbSet RatingSources { get; set; } + + public virtual DbSet Releases { get; set; } + + public virtual DbSet Seasons { get; set; } + + public virtual DbSet SeasonMetadata { get; set; } + + public virtual DbSet Series { get; set; } + + public virtual DbSet SeriesMetadata { get; set; } + + public virtual DbSet Tracks { get; set; } + + public virtual DbSet TrackMetadata { get; set; }*/ + + /// + public override int SaveChanges() + { + foreach (var saveEntity in ChangeTracker.Entries() + .Where(e => e.State == EntityState.Modified) + .Select(entry => entry.Entity) + .OfType()) { + saveEntity.OnSavingChanges(); } - /// - /// Gets or sets the default connection string. - /// - public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db"; + return base.SaveChanges(); + } - public virtual DbSet AccessSchedules { get; set; } + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc); + base.OnModelCreating(modelBuilder); - public virtual DbSet ActivityLogs { get; set; } - - public virtual DbSet ApiKeys { get; set; } - - public virtual DbSet Devices { get; set; } - - public virtual DbSet DeviceOptions { get; set; } - - public virtual DbSet DisplayPreferences { get; set; } - - public virtual DbSet ImageInfos { get; set; } - - public virtual DbSet ItemDisplayPreferences { get; set; } - - public virtual DbSet CustomItemDisplayPreferences { get; set; } - - public virtual DbSet Permissions { get; set; } - - public virtual DbSet Preferences { get; set; } - - public virtual DbSet Users { get; set; } - - /*public virtual DbSet Artwork { get; set; } - - public virtual DbSet Books { get; set; } - - public virtual DbSet BookMetadata { get; set; } - - public virtual DbSet Chapters { get; set; } - - public virtual DbSet Collections { get; set; } - - public virtual DbSet CollectionItems { get; set; } - - public virtual DbSet Companies { get; set; } - - public virtual DbSet CompanyMetadata { get; set; } - - public virtual DbSet CustomItems { get; set; } - - public virtual DbSet CustomItemMetadata { get; set; } - - public virtual DbSet Episodes { get; set; } - - public virtual DbSet EpisodeMetadata { get; set; } - - public virtual DbSet Genres { get; set; } - - public virtual DbSet Groups { get; set; } - - public virtual DbSet Libraries { get; set; } - - public virtual DbSet LibraryItems { get; set; } - - public virtual DbSet LibraryRoot { get; set; } - - public virtual DbSet MediaFiles { get; set; } - - public virtual DbSet MediaFileStream { get; set; } - - public virtual DbSet Metadata { get; set; } - - public virtual DbSet MetadataProviders { get; set; } - - public virtual DbSet MetadataProviderIds { get; set; } - - public virtual DbSet Movies { get; set; } - - public virtual DbSet MovieMetadata { get; set; } - - public virtual DbSet MusicAlbums { get; set; } - - public virtual DbSet MusicAlbumMetadata { get; set; } - - public virtual DbSet People { get; set; } - - public virtual DbSet PersonRoles { get; set; } - - public virtual DbSet Photo { get; set; } - - public virtual DbSet PhotoMetadata { get; set; } - - public virtual DbSet ProviderMappings { get; set; } - - public virtual DbSet Ratings { get; set; } - - /// - /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to - /// store review ratings, not age ratings. - /// - public virtual DbSet RatingSources { get; set; } - - public virtual DbSet Releases { get; set; } - - public virtual DbSet Seasons { get; set; } - - public virtual DbSet SeasonMetadata { get; set; } - - public virtual DbSet Series { get; set; } - - public virtual DbSet SeriesMetadata { get; set; } - - public virtual DbSet Tracks { get; set; } - - public virtual DbSet TrackMetadata { get; set; }*/ - - /// - public override int SaveChanges() - { - foreach (var saveEntity in ChangeTracker.Entries() - .Where(e => e.State == EntityState.Modified) - .Select(entry => entry.Entity) - .OfType()) - { - saveEntity.OnSavingChanges(); - } - - return base.SaveChanges(); - } - - /// - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc); - base.OnModelCreating(modelBuilder); - - // Configuration for each entity is in it's own class inside 'ModelConfiguration'. - modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDb).Assembly); - } + // Configuration for each entity is in it's own class inside 'ModelConfiguration'. + modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDb).Assembly); } } From ab6baf6486601b6693e98339e2a41646196f0e76 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 16 Jan 2023 10:28:31 -0500 Subject: [PATCH 85/97] Enable nullable for Jellyfin DbContext --- Jellyfin.Server.Implementations/JellyfinDb.cs | 110 +++++++++--------- 1 file changed, 52 insertions(+), 58 deletions(-) diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 6443100806..5d3fc9e7db 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -21,118 +20,113 @@ public class JellyfinDb : DbContext { } - /// - /// Gets or sets the default connection string. - /// - public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db"; + public DbSet AccessSchedules => Set(); - public virtual DbSet AccessSchedules { get; set; } + public DbSet ActivityLogs => Set(); - public virtual DbSet ActivityLogs { get; set; } + public DbSet ApiKeys => Set(); - public virtual DbSet ApiKeys { get; set; } + public DbSet Devices => Set(); - public virtual DbSet Devices { get; set; } + public DbSet DeviceOptions => Set(); - public virtual DbSet DeviceOptions { get; set; } + public DbSet DisplayPreferences => Set(); - public virtual DbSet DisplayPreferences { get; set; } + public DbSet ImageInfos => Set(); - public virtual DbSet ImageInfos { get; set; } + public DbSet ItemDisplayPreferences => Set(); - public virtual DbSet ItemDisplayPreferences { get; set; } + public DbSet CustomItemDisplayPreferences => Set(); - public virtual DbSet CustomItemDisplayPreferences { get; set; } + public DbSet Permissions => Set(); - public virtual DbSet Permissions { get; set; } + public DbSet Preferences => Set(); - public virtual DbSet Preferences { get; set; } + public DbSet Users => Set(); - public virtual DbSet Users { get; set; } + /*public DbSet Artwork => Set(); - /*public virtual DbSet Artwork { get; set; } + public DbSet Books => Set(); - public virtual DbSet Books { get; set; } + public DbSet BookMetadata => Set(); - public virtual DbSet BookMetadata { get; set; } + public DbSet Chapters => Set(); - public virtual DbSet Chapters { get; set; } + public DbSet Collections => Set(); - public virtual DbSet Collections { get; set; } + public DbSet CollectionItems => Set(); - public virtual DbSet CollectionItems { get; set; } + public DbSet Companies => Set(); - public virtual DbSet Companies { get; set; } + public DbSet CompanyMetadata => Set(); - public virtual DbSet CompanyMetadata { get; set; } + public DbSet CustomItems => Set(); - public virtual DbSet CustomItems { get; set; } + public DbSet CustomItemMetadata => Set(); - public virtual DbSet CustomItemMetadata { get; set; } + public DbSet Episodes => Set(); - public virtual DbSet Episodes { get; set; } + public DbSet EpisodeMetadata => Set(); - public virtual DbSet EpisodeMetadata { get; set; } + public DbSet Genres => Set(); - public virtual DbSet Genres { get; set; } + public DbSet Groups => Set(); - public virtual DbSet Groups { get; set; } + public DbSet Libraries => Set(); - public virtual DbSet Libraries { get; set; } + public DbSet LibraryItems => Set(); - public virtual DbSet LibraryItems { get; set; } + public DbSet LibraryRoot => Set(); - public virtual DbSet LibraryRoot { get; set; } + public DbSet MediaFiles => Set(); - public virtual DbSet MediaFiles { get; set; } + public DbSet MediaFileStream => Set(); - public virtual DbSet MediaFileStream { get; set; } + public DbSet Metadata => Set(); - public virtual DbSet Metadata { get; set; } + public DbSet MetadataProviders => Set(); - public virtual DbSet MetadataProviders { get; set; } + public DbSet MetadataProviderIds => Set(); - public virtual DbSet MetadataProviderIds { get; set; } + public DbSet Movies => Set(); - public virtual DbSet Movies { get; set; } + public DbSet MovieMetadata => Set(); - public virtual DbSet MovieMetadata { get; set; } + public DbSet MusicAlbums => Set(); - public virtual DbSet MusicAlbums { get; set; } + public DbSet MusicAlbumMetadata => Set(); - public virtual DbSet MusicAlbumMetadata { get; set; } + public DbSet People => Set(); - public virtual DbSet People { get; set; } + public DbSet PersonRoles => Set(); - public virtual DbSet PersonRoles { get; set; } + public DbSet Photo => Set(); - public virtual DbSet Photo { get; set; } + public DbSet PhotoMetadata => Set(); - public virtual DbSet PhotoMetadata { get; set; } + public DbSet ProviderMappings => Set(); - public virtual DbSet ProviderMappings { get; set; } - - public virtual DbSet Ratings { get; set; } + public DbSet Ratings => Set(); /// /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to /// store review ratings, not age ratings. /// - public virtual DbSet RatingSources { get; set; } + public DbSet RatingSources => Set(); - public virtual DbSet Releases { get; set; } + public DbSet Releases => Set(); - public virtual DbSet Seasons { get; set; } + public DbSet Seasons => Set(); - public virtual DbSet SeasonMetadata { get; set; } + public DbSet SeasonMetadata => Set(); - public virtual DbSet Series { get; set; } + public DbSet Series => Set(); - public virtual DbSet SeriesMetadata { get; set; } + public DbSet SeriesMetadata => Set Tracks { get; set; } + public DbSet Tracks => Set(); - public virtual DbSet TrackMetadata { get; set; }*/ + public DbSet TrackMetadata => Set();*/ /// public override int SaveChanges() From f07553abdf9c3b1462a94de154ec0072cdbf686a Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 16 Jan 2023 11:49:59 -0500 Subject: [PATCH 86/97] Optimize EF Core queries and remove unnecessary AsQueryable calls --- .../Activity/ActivityManager.cs | 28 +++++++++---------- .../Devices/DeviceManager.cs | 26 +++++------------ .../Security/AuthenticationManager.cs | 2 -- .../Users/DisplayPreferencesManager.cs | 3 -- .../Users/UserManager.cs | 1 - 5 files changed, 21 insertions(+), 39 deletions(-) diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 9d6ca6aabe..fc03cd6ae4 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -48,18 +48,10 @@ namespace Jellyfin.Server.Implementations.Activity var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - IQueryable entries = dbContext.ActivityLogs - .OrderByDescending(entry => entry.DateCreated); - - if (query.MinDate.HasValue) - { - entries = entries.Where(entry => entry.DateCreated >= query.MinDate); - } - - if (query.HasUserId.HasValue) - { - entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value); - } + var entries = dbContext.ActivityLogs + .OrderByDescending(entry => entry.DateCreated) + .Where(entry => query.MinDate == null || entry.DateCreated >= query.MinDate) + .Where(entry => !query.HasUserId.HasValue || entry.UserId.Equals(default) != query.HasUserId.Value); return new QueryResult( query.Skip, @@ -67,8 +59,16 @@ namespace Jellyfin.Server.Implementations.Activity await entries .Skip(query.Skip ?? 0) .Take(query.Limit ?? 100) - .AsAsyncEnumerable() - .Select(ConvertToOldModel) + .Select(entity => new ActivityLogEntry(entity.Name, entity.Type, entity.UserId) + { + Id = entity.Id, + Overview = entity.Overview, + ShortOverview = entity.ShortOverview, + ItemId = entity.ItemId, + Date = entity.DateCreated, + Severity = entity.LogSeverity + }) + .AsQueryable() .ToListAsync() .ConfigureAwait(false)); } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 15ac5c668a..a9b974be0e 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -54,7 +54,7 @@ namespace Jellyfin.Server.Implementations.Devices var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); + deviceOptions = await dbContext.DeviceOptions.FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); if (deviceOptions is null) { deviceOptions = new DeviceOptions(deviceId); @@ -132,22 +132,11 @@ namespace Jellyfin.Server.Implementations.Devices var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var devices = dbContext.Devices.AsQueryable(); - - if (query.UserId.HasValue) - { - devices = devices.Where(device => device.UserId.Equals(query.UserId.Value)); - } - - if (query.DeviceId is not null) - { - devices = devices.Where(device => device.DeviceId == query.DeviceId); - } - - if (query.AccessToken is not null) - { - devices = devices.Where(device => device.AccessToken == query.AccessToken); - } + var devices = dbContext.Devices + .OrderBy(d => d.Id) + .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value)) + .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId) + .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken); var count = await devices.CountAsync().ConfigureAwait(false); @@ -179,11 +168,10 @@ namespace Jellyfin.Server.Implementations.Devices /// public async Task> GetDevicesForUser(Guid? userId, bool? supportsSync) { - IAsyncEnumerable sessions; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - sessions = dbContext.Devices + IAsyncEnumerable sessions = dbContext.Devices .Include(d => d.User) .OrderByDescending(d => d.DateLastActivity) .ThenBy(d => d.DeviceId) diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index 810e578075..1b56237fe7 100644 --- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -40,7 +40,6 @@ namespace Jellyfin.Server.Implementations.Security await using (dbContext.ConfigureAwait(false)) { return await dbContext.ApiKeys - .AsAsyncEnumerable() .Select(key => new AuthenticationInfo { AppName = key.Name, @@ -60,7 +59,6 @@ namespace Jellyfin.Server.Implementations.Security await using (dbContext.ConfigureAwait(false)) { var key = await dbContext.ApiKeys - .AsQueryable() .Where(apiKey => apiKey.AccessToken == accessToken) .FirstOrDefaultAsync() .ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index fddad1c4f9..8936f57c62 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -62,7 +62,6 @@ namespace Jellyfin.Server.Implementations.Users public IList ListItemDisplayPreferences(Guid userId, string client) { return _dbContext.ItemDisplayPreferences - .AsQueryable() .Where(prefs => prefs.UserId.Equals(userId) && !prefs.ItemId.Equals(default) && string.Equals(prefs.Client, client)) .ToList(); } @@ -71,7 +70,6 @@ namespace Jellyfin.Server.Implementations.Users public Dictionary ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client) { return _dbContext.CustomItemDisplayPreferences - .AsQueryable() .Where(prefs => prefs.UserId.Equals(userId) && prefs.ItemId.Equals(itemId) && string.Equals(prefs.Client, client)) @@ -82,7 +80,6 @@ namespace Jellyfin.Server.Implementations.Users public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary customPreferences) { var existingPrefs = _dbContext.CustomItemDisplayPreferences - .AsQueryable() .Where(prefs => prefs.UserId.Equals(userId) && prefs.ItemId.Equals(itemId) && string.Equals(prefs.Client, client)); diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 19ac007b93..5f9814ed37 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -143,7 +143,6 @@ namespace Jellyfin.Server.Implementations.Users await using (dbContext.ConfigureAwait(false)) { if (await dbContext.Users - .AsQueryable() .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id)) .ConfigureAwait(false)) { From 40e4370689501789974cdd827341da74f4c71c79 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 16 Jan 2023 11:52:53 -0500 Subject: [PATCH 87/97] Specify AsSplitQuery in initial users query --- Jellyfin.Server.Implementations/Users/UserManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 5f9814ed37..266bff5e8d 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -85,6 +85,7 @@ namespace Jellyfin.Server.Implementations.Users _users = new ConcurrentDictionary(); using var dbContext = _dbProvider.CreateDbContext(); foreach (var user in dbContext.Users + .AsSplitQuery() .Include(user => user.Permissions) .Include(user => user.Preferences) .Include(user => user.AccessSchedules) From 66eff8b9ca18ab1cde76196c145ad0a98d0eda99 Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Mon, 16 Jan 2023 18:06:44 +0100 Subject: [PATCH 88/97] Allow limiting parallel image encodings to reduce memory usage (#8783) --- .../Configuration/ServerConfiguration.cs | 6 ++++ src/Jellyfin.Drawing/ImageProcessor.cs | 31 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index d3e042abaa..c39162250a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -259,5 +259,11 @@ namespace MediaBrowser.Model.Configuration /// /// The chapter image resolution. public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource; + + /// + /// Gets or sets the limit for parallel image encoding. + /// + /// The limit for parallel image encoding. + public int ParallelImageEncodingLimit { get; set; } = 0; } } diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index b381c9ae73..353a27b254 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -5,10 +5,12 @@ using System.IO; using System.Linq; using System.Net.Mime; using System.Text; +using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.MediaEncoding; @@ -38,6 +40,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable private readonly IImageEncoder _imageEncoder; private readonly IMediaEncoder _mediaEncoder; + private readonly SemaphoreSlim _parallelEncodingLimit; + private bool _disposed; /// @@ -48,18 +52,28 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable /// The filesystem. /// The image encoder. /// The media encoder. + /// The configuration. public ImageProcessor( ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IImageEncoder imageEncoder, - IMediaEncoder mediaEncoder) + IMediaEncoder mediaEncoder, + IServerConfigurationManager config) { _logger = logger; _fileSystem = fileSystem; _imageEncoder = imageEncoder; _mediaEncoder = mediaEncoder; _appPaths = appPaths; + + var semaphoreCount = config.Configuration.ParallelImageEncodingLimit; + if (semaphoreCount < 1) + { + semaphoreCount = 2 * Environment.ProcessorCount; + } + + _parallelEncodingLimit = new(semaphoreCount, semaphoreCount); } private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); @@ -199,7 +213,18 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable { if (!File.Exists(cacheFilePath)) { - string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat); + // Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage + await _parallelEncodingLimit.WaitAsync().ConfigureAwait(false); + + string resultPath; + try + { + resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat); + } + finally + { + _parallelEncodingLimit.Release(); + } if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase)) { @@ -563,6 +588,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable disposable.Dispose(); } + _parallelEncodingLimit?.Dispose(); + _disposed = true; } } From 3f66a482069ab98396952292db379e7248cb1166 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 16 Jan 2023 12:13:06 -0500 Subject: [PATCH 89/97] Document JellyfinDb --- Jellyfin.Server.Implementations/JellyfinDb.cs | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 5d3fc9e7db..064118ca54 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Linq; using Jellyfin.Data.Entities; @@ -20,28 +18,64 @@ public class JellyfinDb : DbContext { } + /// + /// Gets the containing the access schedules. + /// public DbSet AccessSchedules => Set(); + /// + /// Gets the containing the activity logs. + /// public DbSet ActivityLogs => Set(); + /// + /// Gets the containing the API keys. + /// public DbSet ApiKeys => Set(); + /// + /// Gets the containing the devices. + /// public DbSet Devices => Set(); + /// + /// Gets the containing the device options. + /// public DbSet DeviceOptions => Set(); + /// + /// Gets the containing the display preferences. + /// public DbSet DisplayPreferences => Set(); + /// + /// Gets the containing the image infos. + /// public DbSet ImageInfos => Set(); + /// + /// Gets the containing the item display preferences. + /// public DbSet ItemDisplayPreferences => Set(); + /// + /// Gets the containing the custom item display preferences. + /// public DbSet CustomItemDisplayPreferences => Set(); + /// + /// Gets the containing the permissions. + /// public DbSet Permissions => Set(); + /// + /// Gets the containing the preferences. + /// public DbSet Preferences => Set(); + /// + /// Gets the containing the users. + /// public DbSet Users => Set(); /*public DbSet Artwork => Set(); From 8479f0f90cd8b7180f45340a59d7122755987859 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 16 Jan 2023 12:14:44 -0500 Subject: [PATCH 90/97] Rename JellyfinDb to JellyfinDbContext --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- .../ScheduledTasks/Tasks/OptimizeDatabaseTask.cs | 4 ++-- .../Activity/ActivityManager.cs | 4 ++-- .../Devices/DeviceManager.cs | 4 ++-- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../{JellyfinDb.cs => JellyfinDbContext.cs} | 8 ++++---- .../20200514181226_AddActivityLog.Designer.cs | 2 +- .../Migrations/20200613202153_AddUsers.Designer.cs | 2 +- .../20200728005145_AddDisplayPreferences.Designer.cs | 2 +- ...200905220533_FixDisplayPreferencesIndex.Designer.cs | 2 +- .../20201004171403_AddMaxActiveSessions.Designer.cs | 2 +- ...01204223655_AddCustomDisplayPreferences.Designer.cs | 2 +- .../20210320181425_AddIndexesAndCollations.Designer.cs | 2 +- .../20210407110544_NullableCustomPrefValue.Designer.cs | 2 +- .../Migrations/20210814002109_AddDevices.Designer.cs | 2 +- ...2080052_AddIndexActivityLogsDateCreated.Designer.cs | 2 +- .../Migrations/DesignTimeJellyfinDbFactory.cs | 10 +++++----- .../Migrations/JellyfinDbModelSnapshot.cs | 2 +- .../Security/AuthenticationManager.cs | 4 ++-- .../Security/AuthorizationContext.cs | 4 ++-- .../Users/DisplayPreferencesManager.cs | 4 ++-- Jellyfin.Server.Implementations/Users/UserManager.cs | 8 ++++---- Jellyfin.Server/CoreAppHost.cs | 2 +- .../Migrations/Routines/MigrateActivityLogDb.cs | 4 ++-- .../Migrations/Routines/MigrateAuthenticationDb.cs | 4 ++-- .../Migrations/Routines/MigrateDisplayPreferencesDb.cs | 4 ++-- Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs | 4 ++-- Jellyfin.Server/Program.cs | 2 +- Jellyfin.Server/Startup.cs | 2 +- 29 files changed, 49 insertions(+), 49 deletions(-) rename Jellyfin.Server.Implementations/{JellyfinDb.cs => JellyfinDbContext.cs} (96%) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7b3d07dfc1..0b8a314192 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -654,7 +654,7 @@ namespace Emby.Server.Implementations /// A task representing the service initialization operation. public async Task InitializeServices() { - var jellyfinDb = await Resolve>().CreateDbContextAsync().ConfigureAwait(false); + var jellyfinDb = await Resolve>().CreateDbContextAsync().ConfigureAwait(false); await using (jellyfinDb.ConfigureAwait(false)) { if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any()) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index 1efacd8562..1f3cb9b63f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { private readonly ILogger _logger; private readonly ILocalizationManager _localization; - private readonly IDbContextFactory _provider; + private readonly IDbContextFactory _provider; /// /// Initializes a new instance of the class. @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public OptimizeDatabaseTask( ILogger logger, ILocalizationManager localization, - IDbContextFactory provider) + IDbContextFactory provider) { _logger = logger; _localization = localization; diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index fc03cd6ae4..ce1c54cbb2 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity /// public class ActivityManager : IActivityManager { - private readonly IDbContextFactory _provider; + private readonly IDbContextFactory _provider; /// /// Initializes a new instance of the class. /// /// The Jellyfin database provider. - public ActivityManager(IDbContextFactory provider) + public ActivityManager(IDbContextFactory provider) { _provider = provider; } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index a9b974be0e..8b15d6823d 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Devices /// public class DeviceManager : IDeviceManager { - private readonly IDbContextFactory _dbProvider; + private readonly IDbContextFactory _dbProvider; private readonly IUserManager _userManager; private readonly ConcurrentDictionary _capabilitiesMap = new(); @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Devices /// /// The database provider. /// The user manager. - public DeviceManager(IDbContextFactory dbProvider, IUserManager userManager) + public DeviceManager(IDbContextFactory dbProvider, IUserManager userManager) { _dbProvider = dbProvider; _userManager = userManager; diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 05c6229316..de64911b42 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -29,7 +29,7 @@ public static class ServiceCollectionExtensions .SkipCachingResults(result => result.Value is null || (result.Value is EFTableRows rows && rows.RowsCount == 0))); - serviceCollection.AddPooledDbContextFactory((serviceProvider, opt) => + serviceCollection.AddPooledDbContextFactory((serviceProvider, opt) => { var applicationPaths = serviceProvider.GetRequiredService(); var loggerFactory = serviceProvider.GetRequiredService(); diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs similarity index 96% rename from Jellyfin.Server.Implementations/JellyfinDb.cs rename to Jellyfin.Server.Implementations/JellyfinDbContext.cs index 064118ca54..0d91707e3e 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -8,13 +8,13 @@ using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations; /// -public class JellyfinDb : DbContext +public class JellyfinDbContext : DbContext { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The database context options. - public JellyfinDb(DbContextOptions options) : base(options) + public JellyfinDbContext(DbContextOptions options) : base(options) { } @@ -183,6 +183,6 @@ public class JellyfinDb : DbContext base.OnModelCreating(modelBuilder); // Configuration for each entity is in it's own class inside 'ModelConfiguration'. - modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDb).Assembly); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly); } } diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs index 98a83b7450..4be6c2faa3 100644 --- a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20200514181226_AddActivityLog")] partial class AddActivityLog { diff --git a/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs index 6342ce9cf3..f3254734ad 100644 --- a/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20200613202153_AddUsers")] partial class AddUsers { diff --git a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs index d44707d069..12d6faa8f5 100644 --- a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20200728005145_AddDisplayPreferences")] partial class AddDisplayPreferences { diff --git a/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs index 2234f9d5fd..f1cc208058 100644 --- a/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20200905220533_FixDisplayPreferencesIndex")] partial class FixDisplayPreferencesIndex { diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs index e5c326a326..f134d363c8 100644 --- a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20201004171403_AddMaxActiveSessions")] partial class AddMaxActiveSessions { diff --git a/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs index 10663d0655..ec65205d45 100644 --- a/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20201204223655_AddCustomDisplayPreferences")] partial class AddCustomDisplayPreferences { diff --git a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs index 8696768245..45dad6be68 100644 --- a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20210320181425_AddIndexesAndCollations")] partial class AddIndexesAndCollations { diff --git a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs index d332d19f28..eff84b4574 100644 --- a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20210407110544_NullableCustomPrefValue")] partial class NullableCustomPrefValue { diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs index 7e9566e2ea..ad7c2dd2c9 100644 --- a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20210814002109_AddDevices")] partial class AddDevices { diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs index 03e3f3c921..f9497a3b69 100644 --- a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs +++ b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] [Migration("20221022080052_AddIndexActivityLogsDateCreated")] partial class AddIndexActivityLogsDateCreated { diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs index 72a4a8c3b6..940cf7c5d5 100644 --- a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs +++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs @@ -4,17 +4,17 @@ using Microsoft.EntityFrameworkCore.Design; namespace Jellyfin.Server.Implementations.Migrations { /// - /// The design time factory for . + /// The design time factory for . /// This is only used for the creation of migrations and not during runtime. /// - internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory + internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory { - public JellyfinDb CreateDbContext(string[] args) + public JellyfinDbContext CreateDbContext(string[] args) { - var optionsBuilder = new DbContextOptionsBuilder(); + var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlite("Data Source=jellyfin.db"); - return new JellyfinDb(optionsBuilder.Options); + return new JellyfinDbContext(optionsBuilder.Options); } } } diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 2dd7b094aa..dd5f7f0121 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { - [DbContext(typeof(JellyfinDb))] + [DbContext(typeof(JellyfinDbContext))] partial class JellyfinDbModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index 1b56237fe7..b2dfe60a14 100644 --- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security /// public class AuthenticationManager : IAuthenticationManager { - private readonly IDbContextFactory _dbProvider; + private readonly IDbContextFactory _dbProvider; /// /// Initializes a new instance of the class. /// /// The database provider. - public AuthenticationManager(IDbContextFactory dbProvider) + public AuthenticationManager(IDbContextFactory dbProvider) { _dbProvider = dbProvider; } diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index ec5742bab0..63d3e8a04c 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -16,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security { public class AuthorizationContext : IAuthorizationContext { - private readonly IDbContextFactory _jellyfinDbProvider; + private readonly IDbContextFactory _jellyfinDbProvider; private readonly IUserManager _userManager; private readonly IServerApplicationHost _serverApplicationHost; public AuthorizationContext( - IDbContextFactory jellyfinDb, + IDbContextFactory jellyfinDb, IUserManager userManager, IServerApplicationHost serverApplicationHost) { diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index 8936f57c62..bfae81e4ca 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Users /// public class DisplayPreferencesManager : IDisplayPreferencesManager { - private readonly JellyfinDb _dbContext; + private readonly JellyfinDbContext _dbContext; /// /// Initializes a new instance of the class. /// /// The database context factory. - public DisplayPreferencesManager(IDbContextFactory dbContextFactory) + public DisplayPreferencesManager(IDbContextFactory dbContextFactory) { _dbContext = dbContextFactory.CreateDbContext(); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 266bff5e8d..dc9d78857e 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users /// public class UserManager : IUserManager { - private readonly IDbContextFactory _dbProvider; + private readonly IDbContextFactory _dbProvider; private readonly IEventManager _eventManager; private readonly ICryptoProvider _cryptoProvider; private readonly INetworkManager _networkManager; @@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users /// The image processor. /// The logger. public UserManager( - IDbContextFactory dbProvider, + IDbContextFactory dbProvider, IEventManager eventManager, ICryptoProvider cryptoProvider, INetworkManager networkManager, @@ -172,7 +172,7 @@ namespace Jellyfin.Server.Implementations.Users } } - internal async Task CreateUserInternalAsync(string name, JellyfinDb dbContext) + internal async Task CreateUserInternalAsync(string name, JellyfinDbContext dbContext) { // TODO: Remove after user item data is migrated. var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false) @@ -886,7 +886,7 @@ namespace Jellyfin.Server.Implementations.Users await UpdateUserAsync(user).ConfigureAwait(false); } - private async Task UpdateUserInternalAsync(JellyfinDb dbContext, User user) + private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user) { dbContext.Users.Update(user); _users[user.Id] = user; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index d70b8f3ab7..40cd5a0446 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -107,7 +107,7 @@ namespace Jellyfin.Server yield return typeof(CoreAppHost).Assembly; // Jellyfin.Server.Implementations - yield return typeof(JellyfinDb).Assembly; + yield return typeof(JellyfinDbContext).Assembly; } /// diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index bf66f75ff9..e8a0af9f88 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines private const string DbFilename = "activitylog.db"; private readonly ILogger _logger; - private readonly IDbContextFactory _provider; + private readonly IDbContextFactory _provider; private readonly IServerApplicationPaths _paths; /// @@ -28,7 +28,7 @@ namespace Jellyfin.Server.Migrations.Routines /// The logger. /// The server application paths. /// The database provider. - public MigrateActivityLogDb(ILogger logger, IServerApplicationPaths paths, IDbContextFactory provider) + public MigrateActivityLogDb(ILogger logger, IServerApplicationPaths paths, IDbContextFactory provider) { _logger = logger; _provider = provider; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index bf1ea8233d..09daae0ff9 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -20,7 +20,7 @@ namespace Jellyfin.Server.Migrations.Routines private const string DbFilename = "authentication.db"; private readonly ILogger _logger; - private readonly IDbContextFactory _dbProvider; + private readonly IDbContextFactory _dbProvider; private readonly IServerApplicationPaths _appPaths; private readonly IUserManager _userManager; @@ -33,7 +33,7 @@ namespace Jellyfin.Server.Migrations.Routines /// The user manager. public MigrateAuthenticationDb( ILogger logger, - IDbContextFactory dbProvider, + IDbContextFactory dbProvider, IServerApplicationPaths appPaths, IUserManager userManager) { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 0fad77cfe6..4b692d14f0 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Server.Migrations.Routines private readonly ILogger _logger; private readonly IServerApplicationPaths _paths; - private readonly IDbContextFactory _provider; + private readonly IDbContextFactory _provider; private readonly JsonSerializerOptions _jsonOptions; private readonly IUserManager _userManager; @@ -39,7 +39,7 @@ namespace Jellyfin.Server.Migrations.Routines public MigrateDisplayPreferencesDb( ILogger logger, IServerApplicationPaths paths, - IDbContextFactory provider, + IDbContextFactory provider, IUserManager userManager) { _logger = logger; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 2dbd82e8fd..ea2f033027 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines private readonly ILogger _logger; private readonly IServerApplicationPaths _paths; - private readonly IDbContextFactory _provider; + private readonly IDbContextFactory _provider; private readonly IXmlSerializer _xmlSerializer; /// @@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines public MigrateUserDb( ILogger logger, IServerApplicationPaths paths, - IDbContextFactory provider, + IDbContextFactory provider, IXmlSerializer xmlSerializer) { _logger = logger; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 540375dce6..70d7a07011 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -236,7 +236,7 @@ namespace Jellyfin.Server { _logger.LogInformation("Running query planner optimizations in the database... This might take a while"); // Run before disposing the application - var context = await appHost.ServiceProvider.GetRequiredService>().CreateDbContextAsync().ConfigureAwait(false); + var context = await appHost.ServiceProvider.GetRequiredService>().CreateDbContextAsync().ConfigureAwait(false); await using (context.ConfigureAwait(false)) { if (context.Database.IsSqlite()) diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index f89f81c766..c5f20b205b 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -119,7 +119,7 @@ namespace Jellyfin.Server .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); services.AddHealthChecks() - .AddCheck>(nameof(JellyfinDb)); + .AddCheck>(nameof(JellyfinDbContext)); services.AddHlsPlaylistGenerator(); } From 96c31ed818cf419e2132adbeabc1502d3ab49ac5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 19:53:18 +0000 Subject: [PATCH 91/97] chore(deps): update dependency sharpfuzz to v2.0.1 --- .../Emby.Server.Implementations.Fuzz.csproj | 2 +- fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj index 9c2449da4d..51df09a210 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj +++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj @@ -19,7 +19,7 @@ - + diff --git a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj index 5e7d14b118..226ab60daa 100644 --- a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj +++ b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj @@ -16,7 +16,7 @@ - + From f3e5139cfc05ae0c3968f9b07fabfe8b3d5868e4 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Mon, 16 Jan 2023 18:15:05 -0500 Subject: [PATCH 92/97] Use default ASP.NET Core logger factory for DbContext factory --- .../Extensions/ServiceCollectionExtensions.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index de64911b42..bb8d4dd14f 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using EFCoreSecondLevelCacheInterceptor; using MediaBrowser.Common.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.Extensions; @@ -32,10 +31,8 @@ public static class ServiceCollectionExtensions serviceCollection.AddPooledDbContextFactory((serviceProvider, opt) => { var applicationPaths = serviceProvider.GetRequiredService(); - var loggerFactory = serviceProvider.GetRequiredService(); opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}") - .AddInterceptors(serviceProvider.GetRequiredService()) - .UseLoggerFactory(loggerFactory); + .AddInterceptors(serviceProvider.GetRequiredService()); }); return serviceCollection; From bf055a2f990d90272915bd7d6732bcaa09ac4fce Mon Sep 17 00:00:00 2001 From: Slug-Cat Date: Mon, 16 Jan 2023 06:57:34 +0000 Subject: [PATCH 93/97] Translated using Weblate (Pirate (pr)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pr/ --- Emby.Server.Implementations/Localization/Core/pr.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index 506c14fdc2..466c8a9905 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -13,5 +13,11 @@ "DeviceOfflineWithName": "{0} abandoned ship", "AppDeviceValues": "Captain: {0}, Ship: {1}", "CameraImageUploadedFrom": "Yer looking glass has glimpsed another painting from {0}", - "Collections": "Barrels" + "Collections": "Barrels", + "ItemAddedWithName": "{0} is now with yer treasure", + "Default": "Normal-like", + "FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}", + "Favorites": "Finest Loot", + "ItemRemovedWithName": "{0} was taken from yer treasure", + "LabelIpAddressValue": "Ship's coordinates: {0}" } From c59f2a3c46dd3fad42dd987e04e48d6576373b13 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 17 Jan 2023 14:41:10 -0500 Subject: [PATCH 94/97] Mark CanSelfRestart as Obsolete --- Emby.Server.Implementations/ApplicationHost.cs | 1 - MediaBrowser.Model/System/SystemInfo.cs | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7b40f530c9..3db48e42f2 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1038,7 +1038,6 @@ namespace Emby.Server.Implementations CachePath = ApplicationPaths.CachePath, OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(), OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name, - CanSelfRestart = true, CanLaunchWebBrowser = CanLaunchWebBrowser, TranscodingTempPath = ConfigurationManager.GetTranscodePath(), ServerName = FriendlyName, diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index a82c1c8c0c..9e56849c7c 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -79,8 +79,9 @@ namespace MediaBrowser.Model.System /// /// Gets or sets a value indicating whether this instance can self restart. /// - /// true if this instance can self restart; otherwise, false. - public bool CanSelfRestart { get; set; } + /// true. + [Obsolete("This is always true")] + public bool CanSelfRestart { get; set; } = true; public bool CanLaunchWebBrowser { get; set; } From 971e338b71afe0675934700bd4ac3494f22e6afa Mon Sep 17 00:00:00 2001 From: SuperDumbTM Date: Tue, 17 Jan 2023 09:16:59 +0000 Subject: [PATCH 95/97] Translated using Weblate (Chinese (Traditional, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index baa9ecc1c1..cdc25ec7c7 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -9,15 +9,15 @@ "Channels": "頻道", "ChapterNameValue": "章節 {0}", "Collections": "合輯", - "DeviceOfflineWithName": "{0} 已經斷開連結", + "DeviceOfflineWithName": "{0} 已經斷開連接", "DeviceOnlineWithName": "{0} 已經連接", - "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗", + "FailedLoginAttemptWithUserName": "{0} 登入失敗", "Favorites": "我的最愛", "Folders": "資料夾", "Genres": "風格", "HeaderAlbumArtists": "專輯藝人", "HeaderContinueWatching": "繼續觀看", - "HeaderFavoriteAlbums": "最愛專輯", + "HeaderFavoriteAlbums": "最愛的專輯", "HeaderFavoriteArtists": "最愛的藝人", "HeaderFavoriteEpisodes": "最愛的劇集", "HeaderFavoriteShows": "最愛的節目", @@ -44,10 +44,10 @@ "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知季數", "NewVersionIsAvailable": "新版本的 Jellyfin 伺服器可供下載。", - "NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新", + "NotificationOptionApplicationUpdateAvailable": "有可用的更新", "NotificationOptionApplicationUpdateInstalled": "應用程式已更新", - "NotificationOptionAudioPlayback": "開始播放音頻", - "NotificationOptionAudioPlaybackStopped": "已停止播放音頻", + "NotificationOptionAudioPlayback": "開始播放音訊", + "NotificationOptionAudioPlaybackStopped": "已停止播放音訊", "NotificationOptionCameraImageUploaded": "相片已上傳", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "已添加新内容", From e408da46518e98f0c46c794e75085a8c9417ab00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jan 2023 04:16:59 -0700 Subject: [PATCH 96/97] chore(deps): update dependency microsoft.codeanalysis.bannedapianalyzers to v3.3.4 (#9117) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Emby.Dlna/Emby.Dlna.csproj | 2 +- Emby.Naming/Emby.Naming.csproj | 2 +- Emby.Notifications/Emby.Notifications.csproj | 2 +- Emby.Photos/Emby.Photos.csproj | 2 +- Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +- Jellyfin.Api/Jellyfin.Api.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- Jellyfin.Networking/Jellyfin.Networking.csproj | 2 +- .../Jellyfin.Server.Implementations.csproj | 2 +- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj | 2 +- MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- MediaBrowser.Providers/MediaBrowser.Providers.csproj | 2 +- MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj | 2 +- src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj | 2 +- src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 2 +- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 2 +- .../Jellyfin.MediaEncoding.Hls.csproj | 2 +- .../Jellyfin.MediaEncoding.Keyframes.csproj | 2 +- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 2 +- tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj | 2 +- .../Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj | 2 +- tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj | 2 +- .../Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj | 2 +- .../Jellyfin.MediaEncoding.Hls.Tests.csproj | 2 +- .../Jellyfin.MediaEncoding.Keyframes.Tests.csproj | 2 +- .../Jellyfin.MediaEncoding.Tests.csproj | 2 +- tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj | 2 +- tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj | 2 +- .../Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj | 2 +- tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj | 2 +- .../Jellyfin.Server.Implementations.Tests.csproj | 2 +- .../Jellyfin.Server.Integration.Tests.csproj | 2 +- tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj | 2 +- .../Jellyfin.XbmcMetadata.Tests.csproj | 2 +- 38 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 7ffb7118aa..60e6dd644d 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -28,7 +28,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 80bc57a5d6..3106e22465 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -47,7 +47,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index 138965c89f..eb269183e9 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -23,7 +23,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 34bc8f32f6..ae6bc2db1f 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -26,7 +26,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 7accc3b8ba..7eaef094b5 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -54,7 +54,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index b5444138fb..45725ec3e6 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -27,7 +27,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 7fe6466d4a..540534e1ba 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -29,7 +29,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 975d1c8ce2..2c153d88b5 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index bc437c5d77..b078db0169 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 829f294ce3..9ea8508f24 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -24,7 +24,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 0296974b54..1b0ff27d98 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -49,7 +49,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 6434621c46..20909c9d57 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -51,7 +51,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index de3987b1e2..039127f9e3 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index e33cfc7a12..1233fb1108 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -31,7 +31,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 284e89f1cb..521ba0f107 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -49,7 +49,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 97ad1ffbcb..13de86a929 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -36,7 +36,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 6e82d96d10..c25932a5a1 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index c686b229a6..a62ebf78c7 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -31,7 +31,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index a5bc8eaa7e..7aa9945033 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -23,7 +23,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 9fed8cbd9a..d7c05ea576 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -34,7 +34,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index 32f80812ab..9a025d5586 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -7,7 +7,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index b11bdc4779..fe4e576937 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index c0e0d2b6b3..6966d81d46 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -29,7 +29,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index c74127f044..5110d59176 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -24,7 +24,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 1ddf5139c9..97350fedad 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -24,7 +24,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj index dc4b58fecf..a2ecd60838 100644 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj index 16b18cc85e..313192b241 100644 --- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj +++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj index c20f3dd999..22b0c417b0 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj @@ -21,7 +21,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj index 5cfad93a6a..373a54504e 100644 --- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index f824b6f3b2..a9a0dbc226 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -33,7 +33,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index b6578a7f11..9858623f82 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -26,7 +26,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index f10f9159dd..920f490ed0 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -28,7 +28,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index 3a39daa364..74bf7cb0e0 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -25,7 +25,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj index 6cc998d274..d3292c38eb 100644 --- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -28,7 +28,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 82628d7339..b796e07d1a 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -34,7 +34,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index 006b38a11d..c40f6942b1 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -31,7 +31,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index 771fad6357..a72a6f1855 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -24,7 +24,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj index 0d69c3f611..dc5b5b9e6b 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj +++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj @@ -25,7 +25,7 @@ - + all runtime; build; native; contentfiles; analyzers From 5359d0c404e71923ef6fdce40e631902f33df8ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jan 2023 21:15:43 +0000 Subject: [PATCH 97/97] chore(deps): update github/codeql-action digest to a34ca99 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 238e31f4b4..6a04d13264 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@515828d97454b8354517688ddc5b48402b723750 # v2 + uses: github/codeql-action/init@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@515828d97454b8354517688ddc5b48402b723750 # v2 + uses: github/codeql-action/autobuild@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@515828d97454b8354517688ddc5b48402b723750 # v2 + uses: github/codeql-action/analyze@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2