From e1190d15d6ca0b7cad2b4991e524b35ecca35949 Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Mon, 1 May 2023 20:11:22 +0200 Subject: option to disable and configure inactive session threshold --- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 07f02d187..f619f384c 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -157,6 +157,12 @@ namespace MediaBrowser.Model.Configuration /// The remaining time in minutes. public int MaxAudiobookResume { get; set; } = 5; + /// + /// Gets or sets the threshold in minutes after a inactive session gets closed automatically. + /// + /// The close inactive session threshold in minutes. + public int InactiveSessionThreshold { get; set; } = 10; + /// /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several -- cgit v1.2.3 From ace89e45976a65a365a3d9d7a2ed737a61d584d4 Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Sun, 14 May 2023 15:05:03 +0200 Subject: fix formatting and update summary --- Emby.Server.Implementations/Session/SessionManager.cs | 9 +++++---- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 056c3e4b6..14a62626c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -404,7 +404,7 @@ namespace Emby.Server.Implementations.Session session.LastPlaybackCheckIn = DateTime.UtcNow; } - if (info.IsPaused && session.LastPausedDate.HasValue == false) + if (info.IsPaused && session.LastPausedDate is null) { session.LastPausedDate = DateTime.UtcNow; } @@ -656,9 +656,10 @@ namespace Emby.Server.Implementations.Session private async void CheckForInactiveSteams(object state) { var pausedSessions = Sessions.Where(i => - (i.NowPlayingItem is not null) && - (i.PlayState.IsPaused == true) && - (i.LastPausedDate is not null)).ToList(); + i.NowPlayingItem is not null + && i.PlayState.IsPaused + && i.LastPausedDate is not null) + .ToList(); if (pausedSessions.Count > 0) { diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index f619f384c..40dcf50b7 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -159,8 +159,9 @@ namespace MediaBrowser.Model.Configuration /// /// Gets or sets the threshold in minutes after a inactive session gets closed automatically. + /// If set to 0 the check for inactive sessions gets disabled. /// - /// The close inactive session threshold in minutes. + /// The close inactive session threshold in minutes. 0 to disable. public int InactiveSessionThreshold { get; set; } = 10; /// -- cgit v1.2.3 From ca7d1a13000ad948eebbfdeb40542312f3e37d3e Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Wed, 22 Feb 2023 00:08:35 -0800 Subject: Trickplay generation, manager, storage --- Emby.Server.Implementations/ApplicationHost.cs | 3 + .../Data/SqliteItemRepository.cs | 123 +++++++ Emby.Server.Implementations/Dto/DtoService.cs | 5 + .../MediaEncoding/EncodingHelper.cs | 35 ++ .../MediaEncoding/IMediaEncoder.cs | 27 ++ .../Persistence/IItemRepository.cs | 21 ++ .../Trickplay/ITrickplayManager.cs | 54 +++ MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 173 ++++++++++ .../Configuration/EncodingOptions.cs | 12 + MediaBrowser.Model/Dto/BaseItemDto.cs | 6 + MediaBrowser.Model/Entities/TrickplayTilesInfo.cs | 50 +++ MediaBrowser.Model/Querying/ItemFields.cs | 5 + .../MediaBrowser.Providers.csproj | 1 + .../Trickplay/TrickplayImagesTask.cs | 116 +++++++ .../Trickplay/TrickplayManager.cs | 363 +++++++++++++++++++++ .../Trickplay/TrickplayProvider.cs | 109 +++++++ 16 files changed, 1103 insertions(+) create mode 100644 MediaBrowser.Controller/Trickplay/ITrickplayManager.cs create mode 100644 MediaBrowser.Model/Entities/TrickplayTilesInfo.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayManager.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayProvider.cs (limited to 'MediaBrowser.Model') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7969577bc..1e0bb0cd6 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -78,6 +78,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; @@ -96,6 +97,7 @@ using MediaBrowser.Providers.Lyric; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Subtitles; +using MediaBrowser.Providers.Trickplay; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -591,6 +593,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ca8f605a0..8ec24522b 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -48,6 +48,7 @@ namespace Emby.Server.Implementations.Data { private const string FromText = " from TypedBaseItems A"; private const string ChaptersTableName = "Chapters2"; + private const string TrickplayTableName = "Trickplay"; private const string SaveItemCommandText = @"replace into TypedBaseItems @@ -383,6 +384,8 @@ namespace Emby.Server.Implementations.Data "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", + "create table if not exists " + TrickplayTableName + " (ItemId GUID, Width INT NOT NULL, Height INT NOT NULL, TileWidth INT NOT NULL, TileHeight INT NOT NULL, TileCount INT NOT NULL, Interval INT NOT NULL, Bandwidth INT NOT NULL, PRIMARY KEY (ItemId, Width))", + CreateMediaStreamsTableCommand, CreateMediaAttachmentsTableCommand, @@ -2135,6 +2138,126 @@ namespace Emby.Server.Implementations.Data } } + /// + public Dictionary GetTilesResolutions(Guid itemId) + { + CheckDisposed(); + + var tilesResolutions = new Dictionary(); + using (var connection = GetConnection(true)) + { + using (var statement = PrepareStatement(connection, "select Width,Height,TileWidth,TileHeight,TileCount,Interval,Bandwidth from " + TrickplayTableName + " where ItemId = @ItemId order by Width asc")) + { + statement.TryBind("@ItemId", itemId); + + foreach (var row in statement.ExecuteQuery()) + { + TrickplayTilesInfo tilesInfo = GetTrickplayTilesInfo(row); + tilesResolutions[tilesInfo.Width] = tilesInfo; + } + } + } + + return tilesResolutions; + } + + /// + public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) + { + CheckDisposed(); + + ArgumentNullException.ThrowIfNull(tilesInfo); + + var idBlob = itemId.ToByteArray(); + using (var connection = GetConnection(false)) + { + connection.RunInTransaction( + db => + { + // Delete old tiles info + db.Execute("delete from " + TrickplayTableName + " where ItemId=@ItemId and Width=@Width", idBlob, tilesInfo.Width); + db.Execute( + "insert into " + TrickplayTableName + " values (@ItemId, @Width, @Height, @TileWidth, @TileHeight, @TileCount, @Interval, @Bandwidth)", + idBlob, + tilesInfo.Width, + tilesInfo.Height, + tilesInfo.TileWidth, + tilesInfo.TileHeight, + tilesInfo.TileCount, + tilesInfo.Interval, + tilesInfo.Bandwidth); + }, + TransactionMode); + } + } + + /// + public Dictionary> GetTrickplayManifest(BaseItem item) + { + CheckDisposed(); + + var trickplayManifest = new Dictionary>(); + foreach (var mediaSource in item.GetMediaSources(false)) + { + var mediaSourceId = Guid.Parse(mediaSource.Id); + var tilesResolutions = GetTilesResolutions(mediaSourceId); + + if (tilesResolutions.Count > 0) + { + trickplayManifest[mediaSourceId] = tilesResolutions; + } + } + + return trickplayManifest; + } + + /// + /// Gets the trickplay tiles info. + /// + /// The reader. + /// TrickplayTilesInfo. + private TrickplayTilesInfo GetTrickplayTilesInfo(IReadOnlyList reader) + { + var tilesInfo = new TrickplayTilesInfo(); + + if (reader.TryGetInt32(0, out var width)) + { + tilesInfo.Width = width; + } + + if (reader.TryGetInt32(1, out var height)) + { + tilesInfo.Height = height; + } + + if (reader.TryGetInt32(2, out var tileWidth)) + { + tilesInfo.TileWidth = tileWidth; + } + + if (reader.TryGetInt32(3, out var tileHeight)) + { + tilesInfo.TileHeight = tileHeight; + } + + if (reader.TryGetInt32(4, out var tileCount)) + { + tilesInfo.TileCount = tileCount; + } + + if (reader.TryGetInt32(5, out var interval)) + { + tilesInfo.Interval = interval; + } + + if (reader.TryGetInt32(6, out var bandwidth)) + { + tilesInfo.Bandwidth = bandwidth; + } + + return tilesInfo; + } + private static bool EnableJoinUserData(InternalItemsQuery query) { if (query.User is null) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7a6ed2cb8..10352b6ff 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1058,6 +1058,11 @@ namespace Emby.Server.Implementations.Dto dto.Chapters = _itemRepo.GetChapters(item); } + if (options.ContainsField(ItemFields.Trickplay)) + { + dto.Trickplay = _itemRepo.GetTrickplayManifest(item); + } + if (video.ExtraType.HasValue) { dto.ExtraType = video.ExtraType.Value.ToString(); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b155d674d..0889a90f4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -149,6 +149,36 @@ namespace MediaBrowser.Controller.MediaEncoding return defaultEncoder; } + private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) + { + var defaultEncoder = "mjpeg"; + + if (state.VideoType == VideoType.VideoFile) + { + var hwType = encodingOptions.HardwareAccelerationType; + + var codecMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "vaapi", defaultEncoder + "_vaapi" }, + { "qsv", defaultEncoder + "_qsv" } + }; + + if (!string.IsNullOrEmpty(hwType) + && encodingOptions.EnableHardwareEncoding + && codecMap.ContainsKey(hwType)) + { + var preferredEncoder = codecMap[hwType]; + + if (_mediaEncoder.SupportsEncoder(preferredEncoder)) + { + return preferredEncoder; + } + } + } + + return defaultEncoder; + } + private bool IsVaapiSupported(EncodingJobInfo state) { // vaapi will throw an error with this input @@ -277,6 +307,11 @@ namespace MediaBrowser.Controller.MediaEncoding return GetH264Encoder(state, encodingOptions); } + if (string.Equals(codec, "mjpeg", StringComparison.OrdinalIgnoreCase)) + { + return GetMjpegEncoder(state, encodingOptions); + } + if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index f830b9f29..aa9faa936 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -137,6 +138,32 @@ namespace MediaBrowser.Controller.MediaEncoding /// Location of video image. Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken); + /// + /// Extracts the video images on interval. + /// + /// Input file. + /// Video container type. + /// Media source information. + /// Media stream information. + /// The interval. + /// The maximum width. + /// Allow for hardware acceleration. + /// Allow for hardware encoding. allowHwAccel must also be true. + /// EncodingHelper instance. + /// The cancellation token. + /// Directory where images where extracted. A given image made before another will always be named with a lower number. + Task ExtractVideoImagesOnIntervalAccelerated( + string inputFile, + string container, + MediaSourceInfo mediaSource, + MediaStream imageStream, + TimeSpan interval, + int maxWidth, + bool allowHwAccel, + bool allowHwEncode, + EncodingHelper encodingHelper, + CancellationToken cancellationToken); + /// /// Gets the media info. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 2c52b2b45..11eb4932c 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -61,6 +61,27 @@ namespace MediaBrowser.Controller.Persistence /// The list of chapters to save. void SaveChapters(Guid id, IReadOnlyList chapters); + /// + /// Get available trickplay resolutions and corresponding info. + /// + /// The item. + /// Map of width resolutions to trickplay tiles info. + Dictionary GetTilesResolutions(Guid itemId); + + /// + /// Saves trickplay tiles info. + /// + /// The item. + /// The trickplay tiles info. + void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + + /// + /// Gets trickplay data for an item. + /// + /// The item. + /// A map of media source id to a map of tile width to tile info. + Dictionary> GetTrickplayManifest(BaseItem item); + /// /// Gets the media streams. /// diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs new file mode 100644 index 000000000..bae458f98 --- /dev/null +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Trickplay +{ + /// + /// Interface ITrickplayManager. + /// + public interface ITrickplayManager + { + /// + /// Generate or replace trickplay data. + /// + /// The video. + /// Whether or not existing data should be replaced. + /// CancellationToken to use for operation. + /// Task. + Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken); + + /// + /// Get available trickplay resolutions and corresponding info. + /// + /// The item. + /// Map of width resolutions to trickplay tiles info. + Dictionary GetTilesResolutions(Guid itemId); + + /// + /// Saves trickplay tiles info. + /// + /// The item. + /// The trickplay tiles info. + void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + + /// + /// Gets the trickplay manifest. + /// + /// The item. + /// A map of media source id to a map of tile width to tile info. + Dictionary> GetTrickplayManifest(BaseItem item); + + /// + /// Gets the path to a trickplay tiles image. + /// + /// The item. + /// The width of a single tile. + /// The tile grid's index. + /// The absolute path. + string GetTrickplayTilePath(BaseItem item, int width, int index); + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4e63d205c..7f8ec03fa 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -21,6 +21,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -28,8 +29,10 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using static Nikse.SubtitleEdit.Core.Common.IfoParser; namespace MediaBrowser.MediaEncoding.Encoder { @@ -775,6 +778,176 @@ namespace MediaBrowser.MediaEncoding.Encoder } /// + public Task ExtractVideoImagesOnIntervalAccelerated( + string inputFile, + string container, + MediaSourceInfo mediaSource, + MediaStream imageStream, + TimeSpan interval, + int maxWidth, + bool allowHwAccel, + bool allowHwEncode, + EncodingHelper encodingHelper, + CancellationToken cancellationToken) + { + var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions(); + + // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin. + // Additionally, we must set a few fields without defaults to prevent null pointer exceptions. + if (!allowHwAccel) + { + options.EnableHardwareEncoding = false; + options.HardwareAccelerationType = string.Empty; + options.EnableTonemapping = false; + } + + var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth }; + var jobState = new EncodingJobInfo(TranscodingJobType.Progressive) + { + IsVideoRequest = true, // must be true for InputVideoHwaccelArgs to return non-empty value + MediaSource = mediaSource, + VideoStream = imageStream, + BaseRequest = baseRequest, // GetVideoProcessingFilterParam errors if null + MediaPath = inputFile, + OutputVideoCodec = "mjpeg" + }; + var vidEncoder = options.AllowMjpegEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec; + + // Get input and filter arguments + var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim(); + if (string.IsNullOrWhiteSpace(inputArg)) + { + throw new InvalidOperationException("EncodingHelper returned empty input arguments."); + } + + if (!allowHwAccel) + { + inputArg = "-threads " + _threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled + } + + var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim(); + if (string.IsNullOrWhiteSpace(filterParam) || filterParam.IndexOf("\"", StringComparison.Ordinal) == -1) + { + throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters."); + } + + return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, _threads, cancellationToken); + } + + private async Task ExtractVideoImagesOnIntervalInternal( + string inputArg, + string filterParam, + TimeSpan interval, + string vidEncoder, + int outputThreads, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(inputArg)) + { + throw new InvalidOperationException("Empty or invalid input argument."); + } + + // Output arguments + string fps = "fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(filterParam)) + { + filterParam = "-vf \"" + fps + "\""; + } + else + { + filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ","); + } + + var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(targetDirectory); + var outputPath = Path.Combine(targetDirectory, "%08d.jpg"); + + // Final command arguments + var args = string.Format( + CultureInfo.InvariantCulture, + "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} -f {4} \"{5}\"", + inputArg, + filterParam, + outputThreads, + vidEncoder, + "image2", + outputPath); + + // Start ffmpeg process + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _ffmpegPath, + Arguments = args, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + }, + EnableRaisingEvents = true + }; + + var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + _logger.LogDebug("{ProcessDescription}", processDescription); + + using (var processWrapper = new ProcessWrapper(process, this)) + { + bool ranToCompletion = false; + + await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + StartProcess(processWrapper); + + // Need to give ffmpeg enough time to make all the thumbnails, which could be a while, + // but we still need to detect if the process hangs. + // Making the assumption that as long as new jpegs are showing up, everything is good. + + bool isResponsive = true; + int lastCount = 0; + + while (isResponsive) + { + if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false)) + { + ranToCompletion = true; + break; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var jpegCount = _fileSystem.GetFilePaths(targetDirectory) + .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase)); + + isResponsive = jpegCount > lastCount; + lastCount = jpegCount; + } + + if (!ranToCompletion) + { + _logger.LogInformation("Killing ffmpeg extraction process due to inactivity."); + StopProcess(processWrapper, 1000); + } + } + finally + { + _thumbnailResourcePool.Release(); + } + + var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; + + if (exitCode == -1) + { + _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription); + + throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription)); + } + + return targetDirectory; + } + } + public string GetTimeParameter(long ticks) { var time = TimeSpan.FromTicks(ticks); diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index a53be0fee..e1d9e00b7 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -48,7 +48,9 @@ public class EncodingOptions EnableIntelLowPowerH264HwEncoder = false; EnableIntelLowPowerHevcHwEncoder = false; EnableHardwareEncoding = true; + EnableTrickplayHwAccel = false; AllowHevcEncoding = false; + AllowMjpegEncoding = false; EnableSubtitleExtraction = true; AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; @@ -244,11 +246,21 @@ public class EncodingOptions /// public bool EnableHardwareEncoding { get; set; } + /// + /// Gets or sets a value indicating whether hardware acceleration is enabled for trickplay generation. + /// + public bool EnableTrickplayHwAccel { 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 MJPEG encoding is enabled. + /// + public bool AllowMjpegEncoding { get; set; } + /// /// Gets or sets a value indicating whether subtitle extraction is enabled. /// diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 8fab1ca6d..ab424c6f5 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -568,6 +568,12 @@ namespace MediaBrowser.Model.Dto /// The chapters. public List Chapters { get; set; } + /// + /// Gets or sets the trickplay manifest. + /// + /// The trickplay manifest. + public Dictionary> Trickplay { get; set; } + /// /// Gets or sets the type of the location. /// diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs new file mode 100644 index 000000000..84b6b0322 --- /dev/null +++ b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs @@ -0,0 +1,50 @@ +namespace MediaBrowser.Model.Entities +{ + /// + /// Class TrickplayTilesInfo. + /// + public class TrickplayTilesInfo + { + /// + /// Gets or sets width of an individual tile. + /// + /// The width. + public int Width { get; set; } + + /// + /// Gets or sets height of an individual tile. + /// + /// The height. + public int Height { get; set; } + + /// + /// Gets or sets amount of tiles per row. + /// + /// The tile grid's width. + public int TileWidth { get; set; } + + /// + /// Gets or sets amount of tiles per column. + /// + /// The tile grid's height. + public int TileHeight { get; set; } + + /// + /// Gets or sets total amount of non-black tiles. + /// + /// The tile count. + public int TileCount { get; set; } + + /// + /// Gets or sets interval in milliseconds between each trickplay tile. + /// + /// The interval. + public int Interval { get; set; } + + /// + /// Gets or sets peak bandwith usage in bits per second. + /// + /// The bandwidth. + public int Bandwidth { get; set; } + } +} diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 6fa1d778a..242a1c6e9 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -34,6 +34,11 @@ namespace MediaBrowser.Model.Querying /// Chapters, + /// + /// The trickplay manifest. + /// + Trickplay, + ChildCount, /// diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 6a40833d7..c836c8ed5 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -22,6 +22,7 @@ + diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs new file mode 100644 index 000000000..3d1450a90 --- /dev/null +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace MediaBrowser.Providers.Trickplay +{ + /// + /// Class TrickplayImagesTask. + /// + public class TrickplayImagesTask : IScheduledTask + { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly IServerConfigurationManager _configurationManager; + private readonly ITrickplayManager _trickplayManager; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The library manager. + /// The localization manager. + /// The configuration manager. + /// The trickplay manager. + public TrickplayImagesTask( + ILogger logger, + ILibraryManager libraryManager, + ILocalizationManager localization, + IServerConfigurationManager configurationManager, + ITrickplayManager trickplayManager) + { + _libraryManager = libraryManager; + _logger = logger; + _localization = localization; + _configurationManager = configurationManager; + _trickplayManager = trickplayManager; + } + + /// + public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages"); + + /// + public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription"); + + /// + public string Key => "RefreshTrickplayImages"; + + /// + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + /// + public IEnumerable GetDefaultTriggers() + { + return new[] + { + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerDaily, + TimeOfDayTicks = TimeSpan.FromHours(3).Ticks, + MaxRuntimeTicks = TimeSpan.FromHours(5).Ticks + } + }; + } + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + // TODO: libraryoptions dont run on libraries with trickplay disabled + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = new[] { MediaType.Video }, + IsVirtualItem = false, + IsFolder = false, + Recursive = false + }).OfType public bool EnableHardwareEncoding { get; set; } - /// - /// Gets or sets a value indicating whether hardware acceleration is enabled for trickplay generation. - /// - public bool EnableTrickplayHwAccel { get; set; } - /// /// Gets or sets a value indicating whether HEVC encoding is enabled. /// diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index df6829946..7718f822b 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -36,6 +36,10 @@ namespace MediaBrowser.Model.Configuration public bool ExtractChapterImagesDuringLibraryScan { get; set; } + public bool EnableTrickplayImageExtraction { get; set; } + + public bool ExtractTrickplayImagesDuringLibraryScan { get; set; } + public MediaPathInfo[] PathInfos { get; set; } public bool SaveLocalMetadata { get; set; } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index 62180804f..4b4514897 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Trickplay; @@ -26,6 +27,7 @@ namespace MediaBrowser.Providers.Trickplay private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; private readonly EncodingHelper _encodingHelper; + private readonly ILibraryManager _libraryManager; private static readonly SemaphoreSlim _resourcePool = new(1, 1); @@ -37,18 +39,21 @@ namespace MediaBrowser.Providers.Trickplay /// The media encoder. /// The file systen. /// The encoding helper. + /// The library manager. public TrickplayManager( ILogger logger, IItemRepository itemRepo, IMediaEncoder mediaEncoder, IFileSystem fileSystem, - EncodingHelper encodingHelper) + EncodingHelper encodingHelper, + ILibraryManager libraryManager) { _logger = logger; _itemRepo = itemRepo; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; _encodingHelper = encodingHelper; + _libraryManager = libraryManager; } /// @@ -287,11 +292,15 @@ namespace MediaBrowser.Providers.Trickplay return false; } - /* TODO config options + if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) + { + return false; + } + var libraryOptions = _libraryManager.GetLibraryOptions(video); if (libraryOptions is not null) { - if (!libraryOptions.EnableChapterImageExtraction) + if (!libraryOptions.EnableTrickplayImageExtraction) { return false; } @@ -300,12 +309,6 @@ namespace MediaBrowser.Providers.Trickplay { return false; } - */ - - if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) - { - return false; - } // Can't extract images if there are no video streams return video.GetMediaStreams().Count > 0; diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index be66dea8a..2b3879ca3 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -27,6 +27,7 @@ namespace MediaBrowser.Providers.Trickplay private readonly ILogger _logger; private readonly IServerConfigurationManager _configurationManager; private readonly ITrickplayManager _trickplayManager; + private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. @@ -34,21 +35,24 @@ namespace MediaBrowser.Providers.Trickplay /// The logger. /// The configuration manager. /// The trickplay manager. + /// The library manager. public TrickplayProvider( ILogger logger, IServerConfigurationManager configurationManager, - ITrickplayManager trickplayManager) + ITrickplayManager trickplayManager, + ILibraryManager libraryManager) { _logger = logger; _configurationManager = configurationManager; _trickplayManager = trickplayManager; + _libraryManager = libraryManager; } /// - public string Name => "Trickplay Preview"; + public string Name => "Trickplay Provider"; /// - public int Order => 1000; + public int Order => 100; /// public bool HasChanged(BaseItem item, IDirectoryService directoryService) @@ -95,11 +99,24 @@ namespace MediaBrowser.Providers.Trickplay return FetchInternal(item, options, cancellationToken); } - private async Task FetchInternal(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) + private async Task FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken) { - // TODO: implement all config options --> + var libraryOptions = _libraryManager.GetLibraryOptions(video); + bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan; + bool replace = options.ReplaceAllImages; + + if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false)) + { + _logger.LogDebug("exit refresh: automated - {0} enable scan - {1}", options.IsAutomated, enableDuringScan.GetValueOrDefault(false)); + return ItemUpdateType.None; + } + // TODO: this is always blocking for metadata collection, make non-blocking option - await _trickplayManager.RefreshTrickplayData(item, options.ReplaceAllImages, cancellationToken).ConfigureAwait(false); + if (true) + { + _logger.LogDebug("called refresh"); + await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); + } // The core doesn't need to trigger any save operations over this return ItemUpdateType.None; -- cgit v1.2.3 From 6c649a7e723454e94303d95d178e91b820ba6b50 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Thu, 23 Feb 2023 17:58:34 -0800 Subject: Options --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 ++- .../Configuration/TrickplayOptions.cs | 61 ++++++++++++++++++++++ .../Configuration/TrickplayScanBehavior.cs | 18 +++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 MediaBrowser.Model/Configuration/TrickplayOptions.cs create mode 100644 MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 7f8ec03fa..9b58f83b4 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -853,10 +853,14 @@ namespace MediaBrowser.MediaEncoding.Encoder { filterParam = "-vf \"" + fps + "\""; } - else + else if (filterParam.IndexOf("\"", StringComparison.Ordinal) != -1) { filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ","); } + else + { + filterParam += fps + ","; + } var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(targetDirectory); diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs new file mode 100644 index 000000000..d527baaa4 --- /dev/null +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace MediaBrowser.Model.Configuration +{ + /// + /// Class TrickplayOptions. + /// + public class TrickplayOptions + { + /// + /// Gets or sets a value indicating whether or not to use HW acceleration. + /// + public bool EnableHwAcceleration { get; set; } = false; + + /// + /// Gets or sets the behavior used by trickplay provider on library scan/update. + /// + public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking; + + /// + /// Gets or sets the process priority for the ffmpeg process. + /// + public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal; + + /// + /// Gets or sets the interval, in ms, between each new trickplay image. + /// + public int Interval { get; set; } = 10000; + + /// + /// Gets or sets the target width resolutions, in px, to generates preview images for. + /// + public HashSet WidthResolutions { get; set; } = new HashSet { 320 }; + + /// + /// Gets or sets number of tile images to allow in X dimension. + /// + public int TileWidth { get; set; } = 10; + + /// + /// Gets or sets number of tile images to allow in Y dimension. + /// + public int TileHeight { get; set; } = 10; + + /// + /// Gets or sets the ffmpeg output quality level. + /// + public int Qscale { get; set; } = 10; + + /// + /// Gets or sets the jpeg quality to use for image tiles. + /// + public int JpegQuality { get; set; } = 90; + + /// + /// Gets or sets the number of threads to be used by ffmpeg. + /// + public int ProcessThreads { get; set; } = 0; + } +} diff --git a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs new file mode 100644 index 000000000..799794176 --- /dev/null +++ b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Model.Configuration +{ + /// + /// Enum TrickplayScanBehavior. + /// + public enum TrickplayScanBehavior + { + /// + /// Starts generation, only return once complete. + /// + Blocking, + + /// + /// Start generation, return immediately. + /// + NonBlocking + } +} -- cgit v1.2.3 From 6744e712d3a4fd6f800e5499c90b247787e48cb6 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Sat, 25 Feb 2023 15:59:46 -0800 Subject: Use config values --- .../MediaEncoding/IMediaEncoder.cs | 13 ++++-- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 37 +++++++++++----- .../Configuration/ServerConfiguration.cs | 2 + .../Trickplay/TrickplayImagesTask.cs | 12 ++++-- .../Trickplay/TrickplayManager.cs | 49 +++++++++++++++------- .../Trickplay/TrickplayProvider.cs | 16 ++++--- 6 files changed, 89 insertions(+), 40 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index aa9faa936..f5e3d03cb 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Configuration; @@ -145,10 +146,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// Video container type. /// Media source information. /// Media stream information. - /// The interval. /// The maximum width. + /// The interval. /// Allow for hardware acceleration. - /// Allow for hardware encoding. allowHwAccel must also be true. + /// The input/output thread count for ffmpeg. + /// The qscale value for ffmpeg. + /// The process priority for the ffmpeg process. /// EncodingHelper instance. /// The cancellation token. /// Directory where images where extracted. A given image made before another will always be named with a lower number. @@ -157,10 +160,12 @@ namespace MediaBrowser.Controller.MediaEncoding string container, MediaSourceInfo mediaSource, MediaStream imageStream, - TimeSpan interval, int maxWidth, + TimeSpan interval, bool allowHwAccel, - bool allowHwEncode, + int? threads, + int? qualityScale, + ProcessPriorityClass? priority, EncodingHelper encodingHelper, CancellationToken cancellationToken); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 9b58f83b4..11f42c3f9 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -783,14 +783,17 @@ namespace MediaBrowser.MediaEncoding.Encoder string container, MediaSourceInfo mediaSource, MediaStream imageStream, - TimeSpan interval, int maxWidth, + TimeSpan interval, bool allowHwAccel, - bool allowHwEncode, + int? threads, + int? qualityScale, + ProcessPriorityClass? priority, EncodingHelper encodingHelper, CancellationToken cancellationToken) { var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions(); + threads = threads ?? _threads; // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin. // Additionally, we must set a few fields without defaults to prevent null pointer exceptions. @@ -822,7 +825,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!allowHwAccel) { - inputArg = "-threads " + _threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled + inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled } var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim(); @@ -831,7 +834,7 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters."); } - return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, _threads, cancellationToken); + return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, threads, qualityScale, priority, cancellationToken); } private async Task ExtractVideoImagesOnIntervalInternal( @@ -839,7 +842,9 @@ namespace MediaBrowser.MediaEncoding.Encoder string filterParam, TimeSpan interval, string vidEncoder, - int outputThreads, + int? outputThreads, + int? qualityScale, + ProcessPriorityClass? priority, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(inputArg)) @@ -857,10 +862,6 @@ namespace MediaBrowser.MediaEncoding.Encoder { filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ","); } - else - { - filterParam += fps + ","; - } var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(targetDirectory); @@ -869,11 +870,12 @@ namespace MediaBrowser.MediaEncoding.Encoder // Final command arguments var args = string.Format( CultureInfo.InvariantCulture, - "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} -f {4} \"{5}\"", + "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}-f {5} \"{6}\"", inputArg, filterParam, - outputThreads, + outputThreads.GetValueOrDefault(_threads), vidEncoder, + qualityScale.HasValue ? "-qscale:v " + qualityScale.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty, "image2", outputPath); @@ -904,6 +906,19 @@ namespace MediaBrowser.MediaEncoding.Encoder { StartProcess(processWrapper); + // Set process priority + if (priority.HasValue) + { + try + { + processWrapper.Process.PriorityClass = priority.Value; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", priority.Value, processDescription); + } + } + // Need to give ffmpeg enough time to make all the thumbnails, which could be a while, // but we still need to detect if the process hangs. // Making the assumption that as long as new jpegs are showing up, everything is good. diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 78a310f0b..097eff295 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -264,5 +264,7 @@ namespace MediaBrowser.Model.Configuration /// /// The limit for parallel image encoding. public int ParallelImageEncodingLimit { get; set; } + + public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions(); } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 87ac145d7..a364926c0 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -22,7 +22,6 @@ namespace MediaBrowser.Providers.Trickplay private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; - private readonly IServerConfigurationManager _configurationManager; private readonly ITrickplayManager _trickplayManager; /// @@ -31,19 +30,16 @@ namespace MediaBrowser.Providers.Trickplay /// The logger. /// The library manager. /// The localization manager. - /// The configuration manager. /// The trickplay manager. public TrickplayImagesTask( ILogger logger, ILibraryManager libraryManager, ILocalizationManager localization, - IServerConfigurationManager configurationManager, ITrickplayManager trickplayManager) { _libraryManager = libraryManager; _logger = logger; _localization = localization; - _configurationManager = configurationManager; _trickplayManager = trickplayManager; } @@ -77,6 +73,14 @@ namespace MediaBrowser.Providers.Trickplay public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { // TODO: libraryoptions dont run on libraries with trickplay disabled + /* will this still get all sub-items? should recursive be true? + * from chapterimagestask + * DtoOptions = new DtoOptions(false) + { + EnableImages = false + }, + SourceTypes = new SourceType[] { SourceType.Library }, + */ var items = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = new[] { MediaType.Video }, diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index cb916dfdb..ed2c11281 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -5,11 +5,13 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -28,6 +30,7 @@ namespace MediaBrowser.Providers.Trickplay private readonly IFileSystem _fileSystem; private readonly EncodingHelper _encodingHelper; private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; private static readonly SemaphoreSlim _resourcePool = new(1, 1); @@ -40,13 +43,15 @@ namespace MediaBrowser.Providers.Trickplay /// The file systen. /// The encoding helper. /// The library manager. + /// The server configuration manager. public TrickplayManager( ILogger logger, IItemRepository itemRepo, IMediaEncoder mediaEncoder, IFileSystem fileSystem, EncodingHelper encodingHelper, - ILibraryManager libraryManager) + ILibraryManager libraryManager, + IServerConfigurationManager config) { _logger = logger; _itemRepo = itemRepo; @@ -54,6 +59,7 @@ namespace MediaBrowser.Providers.Trickplay _fileSystem = fileSystem; _encodingHelper = encodingHelper; _libraryManager = libraryManager; + _config = config; } /// @@ -61,16 +67,27 @@ namespace MediaBrowser.Providers.Trickplay { _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); - foreach (var width in new int[] { 320 } /*todo conf*/) + var options = _config.Configuration.TrickplayOptions; + foreach (var width in options.WidthResolutions) { cancellationToken.ThrowIfCancellationRequested(); - await RefreshTrickplayData(video, replace, width, 10000/*todo conf*/, 10/*todo conf*/, 10/*todo conf*/, true/*todo conf*/, true/*todo conf*/, cancellationToken).ConfigureAwait(false); + await RefreshTrickplayDataInternal( + video, + replace, + width, + options, + cancellationToken).ConfigureAwait(false); } } - private async Task RefreshTrickplayData(Video video, bool replace, int width, int interval, int tileWidth, int tileHeight, bool doHwAccel, bool doHwEncode, CancellationToken cancellationToken) + private async Task RefreshTrickplayDataInternal( + Video video, + bool replace, + int width, + TrickplayOptions options, + CancellationToken cancellationToken) { - if (!CanGenerateTrickplay(video, interval)) + if (!CanGenerateTrickplay(video, options.Interval)) { return; } @@ -108,10 +125,12 @@ namespace MediaBrowser.Providers.Trickplay container, mediaSource, mediaStream, - TimeSpan.FromMilliseconds(interval), width, - doHwAccel, - doHwEncode, + TimeSpan.FromMilliseconds(options.Interval), + options.EnableHwAcceleration, + options.ProcessThreads, + options.Qscale, + options.ProcessPriority, _encodingHelper, cancellationToken).ConfigureAwait(false); @@ -127,7 +146,7 @@ namespace MediaBrowser.Providers.Trickplay // Create tiles var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var tilesInfo = CreateTiles(images, width, interval, tileWidth, tileHeight, 100/* todo _config.JpegQuality*/, tilesTempDir, outputDir); + var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); // Save tiles info try @@ -166,7 +185,7 @@ namespace MediaBrowser.Providers.Trickplay } } - private TrickplayTilesInfo CreateTiles(List images, int width, int interval, int tileWidth, int tileHeight, int quality, string workDir, string outputDir) + private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir) { if (images.Count == 0) { @@ -178,9 +197,9 @@ namespace MediaBrowser.Providers.Trickplay var tilesInfo = new TrickplayTilesInfo { Width = width, - Interval = interval, - TileWidth = tileWidth, - TileHeight = tileHeight, + Interval = options.Interval, + TileWidth = options.TileWidth, + TileHeight = options.TileHeight, TileCount = 0, Bandwidth = 0 }; @@ -244,7 +263,7 @@ namespace MediaBrowser.Providers.Trickplay var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg"); using (var stream = File.OpenWrite(tileGridPath)) { - tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, quality); + tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality); } var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); @@ -351,7 +370,7 @@ namespace MediaBrowser.Providers.Trickplay { Directory.Move(source, destination); } - catch (System.IO.IOException) + catch (IOException) { // Cross device move requires a copy Directory.CreateDirectory(destination); diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index e4bd9e3c2..e29646725 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -7,6 +7,7 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Configuration; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Trickplay @@ -25,7 +26,7 @@ namespace MediaBrowser.Providers.Trickplay IForcedProvider { private readonly ILogger _logger; - private readonly IServerConfigurationManager _configurationManager; + private readonly IServerConfigurationManager _config; private readonly ITrickplayManager _trickplayManager; private readonly ILibraryManager _libraryManager; @@ -33,17 +34,17 @@ namespace MediaBrowser.Providers.Trickplay /// Initializes a new instance of the class. /// /// The logger. - /// The configuration manager. + /// The configuration manager. /// The trickplay manager. /// The library manager. public TrickplayProvider( ILogger logger, - IServerConfigurationManager configurationManager, + IServerConfigurationManager config, ITrickplayManager trickplayManager, ILibraryManager libraryManager) { _logger = logger; - _configurationManager = configurationManager; + _config = config; _trickplayManager = trickplayManager; _libraryManager = libraryManager; } @@ -110,11 +111,14 @@ namespace MediaBrowser.Providers.Trickplay return ItemUpdateType.None; } - // TODO: this is always blocking for metadata collection, make non-blocking option - if (true) + if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking) { await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); } + else + { + _ = _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); + } // The core doesn't need to trigger any save operations over this return ItemUpdateType.None; -- cgit v1.2.3 From d84370a6f72111b544f1bbcaa8f3b0339148b5f8 Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Sat, 25 Feb 2023 20:36:38 -0800 Subject: Make trickplay response ids have no dashes --- Emby.Server.Implementations/Dto/DtoService.cs | 4 +++- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 10352b6ff..1687fa442 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1060,7 +1060,9 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Trickplay)) { - dto.Trickplay = _itemRepo.GetTrickplayManifest(item); + // To stay consistent with other fields, this must go from a Guid to a non-dashed string. + // This does not seem to occur automatically to dictionaries like it does with other Guid fields. + dto.Trickplay = _itemRepo.GetTrickplayManifest(item).ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value); } if (video.ExtraType.HasValue) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index ab424c6f5..3db9cb08b 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -572,7 +572,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the trickplay manifest. /// /// The trickplay manifest. - public Dictionary> Trickplay { get; set; } + public Dictionary> Trickplay { get; set; } /// /// Gets or sets the type of the location. -- cgit v1.2.3 From 2e2085a212312282e20dcb6578503b6daff9b72e Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Sat, 4 Mar 2023 16:19:09 -0800 Subject: HashSet datatype was causing default values to always be added on server start --- MediaBrowser.Model/Configuration/TrickplayOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs index d527baaa4..f85259a14 100644 --- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Configuration /// /// Gets or sets the target width resolutions, in px, to generates preview images for. /// - public HashSet WidthResolutions { get; set; } = new HashSet { 320 }; + public int[] WidthResolutions { get; set; } = new[] { 320 }; /// /// Gets or sets number of tile images to allow in X dimension. @@ -51,7 +51,7 @@ namespace MediaBrowser.Model.Configuration /// /// Gets or sets the jpeg quality to use for image tiles. /// - public int JpegQuality { get; set; } = 90; + public int JpegQuality { get; set; } = 80; /// /// Gets or sets the number of threads to be used by ffmpeg. -- cgit v1.2.3 From b89bf5d735de144495b3dc9acbcf70c17fb24c15 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Tue, 28 Mar 2023 13:09:34 -0700 Subject: Change defaults --- MediaBrowser.Model/Configuration/TrickplayOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs index f85259a14..d89e5f590 100644 --- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -46,12 +46,12 @@ namespace MediaBrowser.Model.Configuration /// /// Gets or sets the ffmpeg output quality level. /// - public int Qscale { get; set; } = 10; + public int Qscale { get; set; } = 4; /// /// Gets or sets the jpeg quality to use for image tiles. /// - public int JpegQuality { get; set; } = 80; + public int JpegQuality { get; set; } = 90; /// /// Gets or sets the number of threads to be used by ffmpeg. -- cgit v1.2.3 From 33770322282326304b4b8073f583d6fed2354c0b Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Mon, 1 May 2023 12:51:05 -0700 Subject: crobibero styling, format, code suggestions --- .../MediaEncoding/EncodingHelper.cs | 27 +- .../Trickplay/ITrickplayManager.cs | 77 ++- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- .../Configuration/TrickplayOptions.cs | 109 ++-- .../Configuration/TrickplayScanBehavior.cs | 25 +- MediaBrowser.Model/Entities/TrickplayTilesInfo.cs | 79 ++- .../Trickplay/TrickplayImagesTask.cs | 147 +++--- .../Trickplay/TrickplayManager.cs | 569 ++++++++++----------- .../Trickplay/TrickplayProvider.cs | 181 ++++--- 9 files changed, 602 insertions(+), 614 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index bcdf2934a..01b6e31e9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -90,6 +90,13 @@ namespace MediaBrowser.Controller.MediaEncoding { "truehd", 6 }, }; + private static readonly string _defaultMjpegEncoder = "mjpeg"; + private static readonly Dictionary _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase) + { + { "vaapi", _defaultMjpegEncoder + "_vaapi" }, + { "qsv", _defaultMjpegEncoder + "_qsv" } + }; + public static readonly string[] LosslessAudioCodecs = new string[] { "alac", @@ -151,32 +158,20 @@ namespace MediaBrowser.Controller.MediaEncoding private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) { - var defaultEncoder = "mjpeg"; - if (state.VideoType == VideoType.VideoFile) { var hwType = encodingOptions.HardwareAccelerationType; - var codecMap = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "vaapi", defaultEncoder + "_vaapi" }, - { "qsv", defaultEncoder + "_qsv" } - }; - if (!string.IsNullOrEmpty(hwType) && encodingOptions.EnableHardwareEncoding - && codecMap.ContainsKey(hwType)) + && _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder) + && _mediaEncoder.SupportsEncoder(preferredEncoder)) { - var preferredEncoder = codecMap[hwType]; - - if (_mediaEncoder.SupportsEncoder(preferredEncoder)) - { - return preferredEncoder; - } + return preferredEncoder; } } - return defaultEncoder; + return _defaultMjpegEncoder; } private bool IsVaapiSupported(EncodingJobInfo state) diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index bae458f98..8e82c57d4 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -5,50 +5,49 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Controller.Trickplay +namespace MediaBrowser.Controller.Trickplay; + +/// +/// Interface ITrickplayManager. +/// +public interface ITrickplayManager { /// - /// Interface ITrickplayManager. + /// Generate or replace trickplay data. /// - public interface ITrickplayManager - { - /// - /// Generate or replace trickplay data. - /// - /// The video. - /// Whether or not existing data should be replaced. - /// CancellationToken to use for operation. - /// Task. - Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken); + /// The video. + /// Whether or not existing data should be replaced. + /// CancellationToken to use for operation. + /// Task. + Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken); - /// - /// Get available trickplay resolutions and corresponding info. - /// - /// The item. - /// Map of width resolutions to trickplay tiles info. - Dictionary GetTilesResolutions(Guid itemId); + /// + /// Get available trickplay resolutions and corresponding info. + /// + /// The item. + /// Map of width resolutions to trickplay tiles info. + Dictionary GetTilesResolutions(Guid itemId); - /// - /// Saves trickplay tiles info. - /// - /// The item. - /// The trickplay tiles info. - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + /// + /// Saves trickplay tiles info. + /// + /// The item. + /// The trickplay tiles info. + void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); - /// - /// Gets the trickplay manifest. - /// - /// The item. - /// A map of media source id to a map of tile width to tile info. - Dictionary> GetTrickplayManifest(BaseItem item); + /// + /// Gets the trickplay manifest. + /// + /// The item. + /// A map of media source id to a map of tile width to tile info. + Dictionary> GetTrickplayManifest(BaseItem item); - /// - /// Gets the path to a trickplay tiles image. - /// - /// The item. - /// The width of a single tile. - /// The tile grid's index. - /// The absolute path. - string GetTrickplayTilePath(BaseItem item, int width, int index); - } + /// + /// Gets the path to a trickplay tiles image. + /// + /// The item. + /// The width of a single tile. + /// The tile grid's index. + /// The absolute path. + string GetTrickplayTilePath(BaseItem item, int width, int index); } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4692bf504..000831fe2 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -793,7 +793,7 @@ namespace MediaBrowser.MediaEncoding.Encoder CancellationToken cancellationToken) { var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions(); - threads = threads ?? _threads; + threads ??= _threads; // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin. // Additionally, we must set a few fields without defaults to prevent null pointer exceptions. diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs index d89e5f590..1fff1a5ed 100644 --- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -1,61 +1,60 @@ using System.Collections.Generic; using System.Diagnostics; -namespace MediaBrowser.Model.Configuration +namespace MediaBrowser.Model.Configuration; + +/// +/// Class TrickplayOptions. +/// +public class TrickplayOptions { /// - /// Class TrickplayOptions. - /// - public class TrickplayOptions - { - /// - /// Gets or sets a value indicating whether or not to use HW acceleration. - /// - public bool EnableHwAcceleration { get; set; } = false; - - /// - /// Gets or sets the behavior used by trickplay provider on library scan/update. - /// - public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking; - - /// - /// Gets or sets the process priority for the ffmpeg process. - /// - public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal; - - /// - /// Gets or sets the interval, in ms, between each new trickplay image. - /// - public int Interval { get; set; } = 10000; - - /// - /// Gets or sets the target width resolutions, in px, to generates preview images for. - /// - public int[] WidthResolutions { get; set; } = new[] { 320 }; - - /// - /// Gets or sets number of tile images to allow in X dimension. - /// - public int TileWidth { get; set; } = 10; - - /// - /// Gets or sets number of tile images to allow in Y dimension. - /// - public int TileHeight { get; set; } = 10; - - /// - /// Gets or sets the ffmpeg output quality level. - /// - public int Qscale { get; set; } = 4; - - /// - /// Gets or sets the jpeg quality to use for image tiles. - /// - public int JpegQuality { get; set; } = 90; - - /// - /// Gets or sets the number of threads to be used by ffmpeg. - /// - public int ProcessThreads { get; set; } = 0; - } + /// Gets or sets a value indicating whether or not to use HW acceleration. + /// + public bool EnableHwAcceleration { get; set; } = false; + + /// + /// Gets or sets the behavior used by trickplay provider on library scan/update. + /// + public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking; + + /// + /// Gets or sets the process priority for the ffmpeg process. + /// + public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal; + + /// + /// Gets or sets the interval, in ms, between each new trickplay image. + /// + public int Interval { get; set; } = 10000; + + /// + /// Gets or sets the target width resolutions, in px, to generates preview images for. + /// + public int[] WidthResolutions { get; set; } = new[] { 320 }; + + /// + /// Gets or sets number of tile images to allow in X dimension. + /// + public int TileWidth { get; set; } = 10; + + /// + /// Gets or sets number of tile images to allow in Y dimension. + /// + public int TileHeight { get; set; } = 10; + + /// + /// Gets or sets the ffmpeg output quality level. + /// + public int Qscale { get; set; } = 4; + + /// + /// Gets or sets the jpeg quality to use for image tiles. + /// + public int JpegQuality { get; set; } = 90; + + /// + /// Gets or sets the number of threads to be used by ffmpeg. + /// + public int ProcessThreads { get; set; } = 0; } diff --git a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs index 799794176..d0db53218 100644 --- a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs +++ b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs @@ -1,18 +1,17 @@ -namespace MediaBrowser.Model.Configuration +namespace MediaBrowser.Model.Configuration; + +/// +/// Enum TrickplayScanBehavior. +/// +public enum TrickplayScanBehavior { /// - /// Enum TrickplayScanBehavior. + /// Starts generation, only return once complete. /// - public enum TrickplayScanBehavior - { - /// - /// Starts generation, only return once complete. - /// - Blocking, + Blocking, - /// - /// Start generation, return immediately. - /// - NonBlocking - } + /// + /// Start generation, return immediately. + /// + NonBlocking } diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs index 84b6b0322..86d37787f 100644 --- a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs +++ b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs @@ -1,50 +1,49 @@ -namespace MediaBrowser.Model.Entities +namespace MediaBrowser.Model.Entities; + +/// +/// Class TrickplayTilesInfo. +/// +public class TrickplayTilesInfo { /// - /// Class TrickplayTilesInfo. + /// Gets or sets width of an individual tile. /// - public class TrickplayTilesInfo - { - /// - /// Gets or sets width of an individual tile. - /// - /// The width. - public int Width { get; set; } + /// The width. + public int Width { get; set; } - /// - /// Gets or sets height of an individual tile. - /// - /// The height. - public int Height { get; set; } + /// + /// Gets or sets height of an individual tile. + /// + /// The height. + public int Height { get; set; } - /// - /// Gets or sets amount of tiles per row. - /// - /// The tile grid's width. - public int TileWidth { get; set; } + /// + /// Gets or sets amount of tiles per row. + /// + /// The tile grid's width. + public int TileWidth { get; set; } - /// - /// Gets or sets amount of tiles per column. - /// - /// The tile grid's height. - public int TileHeight { get; set; } + /// + /// Gets or sets amount of tiles per column. + /// + /// The tile grid's height. + public int TileHeight { get; set; } - /// - /// Gets or sets total amount of non-black tiles. - /// - /// The tile count. - public int TileCount { get; set; } + /// + /// Gets or sets total amount of non-black tiles. + /// + /// The tile count. + public int TileCount { get; set; } - /// - /// Gets or sets interval in milliseconds between each trickplay tile. - /// - /// The interval. - public int Interval { get; set; } + /// + /// Gets or sets interval in milliseconds between each trickplay tile. + /// + /// The interval. + public int Interval { get; set; } - /// - /// Gets or sets peak bandwith usage in bits per second. - /// - /// The bandwidth. - public int Bandwidth { get; set; } - } + /// + /// Gets or sets peak bandwith usage in bits per second. + /// + /// The bandwidth. + public int Bandwidth { get; set; } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 8d0d9d5a3..f32557cd1 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -12,98 +12,97 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Trickplay +namespace MediaBrowser.Providers.Trickplay; + +/// +/// Class TrickplayImagesTask. +/// +public class TrickplayImagesTask : IScheduledTask { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly ITrickplayManager _trickplayManager; + /// - /// Class TrickplayImagesTask. + /// Initializes a new instance of the class. /// - public class TrickplayImagesTask : IScheduledTask + /// The logger. + /// The library manager. + /// The localization manager. + /// The trickplay manager. + public TrickplayImagesTask( + ILogger logger, + ILibraryManager libraryManager, + ILocalizationManager localization, + ITrickplayManager trickplayManager) { - private readonly ILogger _logger; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; - private readonly ITrickplayManager _trickplayManager; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The library manager. - /// The localization manager. - /// The trickplay manager. - public TrickplayImagesTask( - ILogger logger, - ILibraryManager libraryManager, - ILocalizationManager localization, - ITrickplayManager trickplayManager) - { - _libraryManager = libraryManager; - _logger = logger; - _localization = localization; - _trickplayManager = trickplayManager; - } + _libraryManager = libraryManager; + _logger = logger; + _localization = localization; + _trickplayManager = trickplayManager; + } - /// - public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages"); + /// + public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages"); - /// - public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription"); + /// + public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription"); - /// - public string Key => "RefreshTrickplayImages"; + /// + public string Key => "RefreshTrickplayImages"; - /// - public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + /// + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); - /// - public IEnumerable GetDefaultTriggers() + /// + public IEnumerable GetDefaultTriggers() + { + return new[] { - return new[] + new TaskTriggerInfo { - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerDaily, - TimeOfDayTicks = TimeSpan.FromHours(3).Ticks - } - }; - } + Type = TaskTriggerInfo.TriggerDaily, + TimeOfDayTicks = TimeSpan.FromHours(3).Ticks + } + }; + } - /// - public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery { - var items = _libraryManager.GetItemList(new InternalItemsQuery - { - MediaTypes = new[] { MediaType.Video }, - IsVirtualItem = false, - IsFolder = false, - Recursive = true - }).OfType /// StreamState of the current stream. - /// Dictionary of widths to corresponding tiles info. + /// Dictionary of widths to corresponding tiles info. /// StringBuilder to append the field to. /// Http user context. - private void AddTrickplay(StreamState state, Dictionary tilesResolutions, StringBuilder builder, ClaimsPrincipal user) + private void AddTrickplay(StreamState state, Dictionary trickplayResolutions, StringBuilder builder, ClaimsPrincipal user) { const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\""; - foreach (var resolution in tilesResolutions) + foreach (var resolution in trickplayResolutions) { var width = resolution.Key; - var tilesInfo = resolution.Value; + var trickplayInfo = resolution.Value; var url = string.Format( CultureInfo.InvariantCulture, @@ -566,9 +566,9 @@ public class DynamicHlsHelper var line = string.Format( CultureInfo.InvariantCulture, playlistFormat, - tilesInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), - tilesInfo.Width.ToString(CultureInfo.InvariantCulture), - tilesInfo.Height.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Width.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Height.ToString(CultureInfo.InvariantCulture), url); builder.AppendLine(line); diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs new file mode 100644 index 000000000..64e7da1b5 --- /dev/null +++ b/Jellyfin.Data/Entities/TrickplayInfo.cs @@ -0,0 +1,75 @@ +using System; +using System.Text.Json.Serialization; + +namespace Jellyfin.Data.Entities; + +/// +/// An entity representing the metadata for a group of trickplay tiles. +/// +public class TrickplayInfo +{ + /// + /// Gets or sets the id of the associated item. + /// + /// + /// Required. + /// + [JsonIgnore] + public Guid ItemId { get; set; } + + /// + /// Gets or sets width of an individual thumbnail. + /// + /// + /// Required. + /// + public int Width { get; set; } + + /// + /// Gets or sets height of an individual thumbnail. + /// + /// + /// Required. + /// + public int Height { get; set; } + + /// + /// Gets or sets amount of thumbnails per row. + /// + /// + /// Required. + /// + public int TileWidth { get; set; } + + /// + /// Gets or sets amount of thumbnails per column. + /// + /// + /// Required. + /// + public int TileHeight { get; set; } + + /// + /// Gets or sets total amount of non-black thumbnails. + /// + /// + /// Required. + /// + public int ThumbnailCount { get; set; } + + /// + /// Gets or sets interval in milliseconds between each trickplay thumbnail. + /// + /// + /// Required. + /// + public int Interval { get; set; } + + /// + /// Gets or sets peak bandwith usage in bits per second. + /// + /// + /// Required. + /// + public int Bandwidth { get; set; } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 0d91707e3..ea99af004 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext /// public DbSet Users => Set(); + /// + /// Gets the containing the trickplay metadata. + /// + public DbSet TrickplayInfos => Set(); + /*public DbSet Artwork => Set(); public DbSet Books => Set(); diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs new file mode 100644 index 000000000..28baf1992 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs @@ -0,0 +1,681 @@ +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20230626233818_AddTrickplayInfos")] + partial class AddTrickplayInfos + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs new file mode 100644 index 000000000..76b12de08 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class AddTrickplayInfos : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TrickplayInfos", + columns: table => new + { + ItemId = table.Column(type: "TEXT", nullable: false), + Width = table.Column(type: "INTEGER", nullable: false), + Height = table.Column(type: "INTEGER", nullable: false), + TileWidth = table.Column(type: "INTEGER", nullable: false), + TileHeight = table.Column(type: "INTEGER", nullable: false), + ThumbnailCount = table.Column(type: "INTEGER", nullable: false), + Interval = table.Column(type: "INTEGER", nullable: false), + Bandwidth = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TrickplayInfos"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index d23508096..3c06e1cfc 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Jellyfin.Server.Implementations; using Microsoft.EntityFrameworkCore; @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("DeviceOptions"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => { b.Property("Id") diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs new file mode 100644 index 000000000..dc1c17e5e --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs @@ -0,0 +1,18 @@ +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration +{ + /// + /// FluentAPI configuration for the TrickplayInfo entity. + /// + public class TrickplayInfoConfiguration : IEntityTypeConfiguration + { + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(info => new { info.ItemId, info.Width }); + } + } +} diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs new file mode 100644 index 000000000..34b27e21a --- /dev/null +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -0,0 +1,468 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Trickplay; + +/// +/// ITrickplayManager implementation. +/// +public class TrickplayManager : ITrickplayManager +{ + private readonly ILogger _logger; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly EncodingHelper _encodingHelper; + private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; + private readonly IImageEncoder _imageEncoder; + private readonly IDbContextFactory _dbProvider; + + private static readonly SemaphoreSlim _resourcePool = new(1, 1); + private static readonly string[] _trickplayImgExtensions = { ".jpg" }; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The media encoder. + /// The file systen. + /// The encoding helper. + /// The library manager. + /// The server configuration manager. + /// The image encoder. + /// The database provider. + public TrickplayManager( + ILogger logger, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + EncodingHelper encodingHelper, + ILibraryManager libraryManager, + IServerConfigurationManager config, + IImageEncoder imageEncoder, + IDbContextFactory dbProvider) + { + _logger = logger; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _encodingHelper = encodingHelper; + _libraryManager = libraryManager; + _config = config; + _imageEncoder = imageEncoder; + _dbProvider = dbProvider; + } + + /// + public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken) + { + _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); + + var options = _config.Configuration.TrickplayOptions; + foreach (var width in options.WidthResolutions) + { + cancellationToken.ThrowIfCancellationRequested(); + await RefreshTrickplayDataInternal( + video, + replace, + width, + options, + cancellationToken).ConfigureAwait(false); + } + } + + private async Task RefreshTrickplayDataInternal( + Video video, + bool replace, + int width, + TrickplayOptions options, + CancellationToken cancellationToken) + { + if (!CanGenerateTrickplay(video, options.Interval)) + { + return; + } + + var imgTempDir = string.Empty; + var outputDir = GetTrickplayDirectory(video, width); + + await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width)) + { + _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); + return; + } + + // Extract images + // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. + var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); + + if (mediaSource is null) + { + _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); + return; + } + + var mediaPath = mediaSource.Path; + var mediaStream = mediaSource.VideoStream; + var container = mediaSource.Container; + + _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); + imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( + mediaPath, + container, + mediaSource, + mediaStream, + width, + TimeSpan.FromMilliseconds(options.Interval), + options.EnableHwAcceleration, + options.ProcessThreads, + options.Qscale, + options.ProcessPriority, + _encodingHelper, + cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) + { + throw new InvalidOperationException("Null or invalid directory from media encoder."); + } + + var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false) + .Select(i => i.FullName) + .OrderBy(i => i) + .ToList(); + + // Create tiles + var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); + var trickplayInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); + + // Save tiles info + try + { + if (trickplayInfo is not null) + { + trickplayInfo.ItemId = video.Id; + await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false); + + _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); + } + else + { + throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while saving trickplay tiles info."); + + // Make sure no files stay in metadata folders on failure + // if tiles info wasn't saved. + Directory.Delete(outputDir, true); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating trickplay images."); + } + finally + { + _resourcePool.Release(); + + if (!string.IsNullOrEmpty(imgTempDir)) + { + Directory.Delete(imgTempDir, true); + } + } + } + + private TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir) + { + if (images.Count == 0) + { + throw new ArgumentException("Can't create trickplay from 0 images."); + } + + Directory.CreateDirectory(workDir); + + var trickplayInfo = new TrickplayInfo + { + Width = width, + Interval = options.Interval, + TileWidth = options.TileWidth, + TileHeight = options.TileHeight, + ThumbnailCount = images.Count, + // Set during image generation + Height = 0, + Bandwidth = 0 + }; + + /* + * Generate trickplay tiles from sets of thumbnails + */ + var imageOptions = new ImageCollageOptions + { + Width = trickplayInfo.TileWidth, + Height = trickplayInfo.TileHeight + }; + + var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight; + var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile); + + for (int i = 0; i < requiredTiles; i++) + { + // Set output/input paths + var tilePath = Path.Combine(workDir, $"{i}.jpg"); + + imageOptions.OutputPath = tilePath; + imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(thumbnailsPerTile).ToList(); + + // Generate image and use returned height for tiles info + var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null); + if (trickplayInfo.Height == 0) + { + trickplayInfo.Height = height; + } + + // Update bitrate + var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000)); + trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate); + } + + /* + * Move trickplay tiles to output directory + */ + Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); + + // Replace existing tiles if they already exist + if (Directory.Exists(outputDir)) + { + Directory.Delete(outputDir, true); + } + + MoveDirectory(workDir, outputDir); + + return trickplayInfo; + } + + private bool CanGenerateTrickplay(Video video, int interval) + { + var videoType = video.VideoType; + if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) + { + return false; + } + + if (video.IsPlaceHolder) + { + return false; + } + + if (video.IsShortcut) + { + return false; + } + + if (!video.IsCompleteMedia) + { + return false; + } + + if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) + { + return false; + } + + var libraryOptions = _libraryManager.GetLibraryOptions(video); + if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction) + { + return false; + } + + // Can't extract images if there are no video streams + return video.GetMediaStreams().Count > 0; + } + + /// + public async Task> GetTrickplayResolutions(Guid itemId) + { + var trickplayResolutions = new Dictionary(); + + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var trickplayInfos = await dbContext.TrickplayInfos + .AsNoTracking() + .Where(i => i.ItemId.Equals(itemId)) + .ToListAsync() + .ConfigureAwait(false); + + foreach (var info in trickplayInfos) + { + trickplayResolutions[info.Width] = info; + } + } + + return trickplayResolutions; + } + + /// + public async Task SaveTrickplayInfo(TrickplayInfo info) + { + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var oldInfo = await dbContext.TrickplayInfos.FindAsync(info.ItemId, info.Width).ConfigureAwait(false); + if (oldInfo is not null) + { + dbContext.TrickplayInfos.Remove(oldInfo); + } + + dbContext.Add(info); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + } + + /// + public async Task>> GetTrickplayManifest(BaseItem item) + { + var trickplayManifest = new Dictionary>(); + foreach (var mediaSource in item.GetMediaSources(false)) + { + var mediaSourceId = Guid.Parse(mediaSource.Id); + var trickplayResolutions = await GetTrickplayResolutions(mediaSourceId).ConfigureAwait(false); + + if (trickplayResolutions.Count > 0) + { + trickplayManifest[mediaSourceId] = trickplayResolutions; + } + } + + return trickplayManifest; + } + + /// + public string GetTrickplayTilePath(BaseItem item, int width, int index) + { + return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); + } + + /// + public async Task GetHlsPlaylist(Guid itemId, int width, string? apiKey) + { + var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false); + if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo)) + { + var builder = new StringBuilder(128); + + if (trickplayInfo.ThumbnailCount > 0) + { + const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; + const string decimalFormat = "{0:0.###}"; + + var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}"; + var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}"; + var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight; + var thumbnailDuration = trickplayInfo.Interval / 1000d; + var infDuration = thumbnailDuration * thumbnailsPerTile; + var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile); + + builder + .AppendLine("#EXTM3U") + .Append("#EXT-X-TARGETDURATION:") + .AppendLine(tileCount.ToString(CultureInfo.InvariantCulture)) + .AppendLine("#EXT-X-VERSION:7") + .AppendLine("#EXT-X-MEDIA-SEQUENCE:1") + .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") + .AppendLine("#EXT-X-IMAGES-ONLY"); + + for (int i = 0; i < tileCount; i++) + { + // All tiles prior to the last must contain full amount of thumbnails (no black). + if (i == tileCount - 1) + { + thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile); + infDuration = thumbnailDuration * thumbnailsPerTile; + } + + // EXTINF + builder + .Append("#EXTINF:") + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration) + .AppendLine(","); + + // EXT-X-TILES + builder + .Append("#EXT-X-TILES:RESOLUTION=") + .Append(resolution) + .Append(",LAYOUT=") + .Append(layout) + .Append(",DURATION=") + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration) + .AppendLine(); + + // URL + builder + .AppendFormat( + CultureInfo.InvariantCulture, + urlFormat, + width.ToString(CultureInfo.InvariantCulture), + i.ToString(CultureInfo.InvariantCulture), + itemId.ToString("N"), + apiKey) + .AppendLine(); + } + + builder.AppendLine("#EXT-X-ENDLIST"); + return builder.ToString(); + } + } + + return null; + } + + private string GetTrickplayDirectory(BaseItem item, int? width = null) + { + var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + + return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; + } + + private void MoveDirectory(string source, string destination) + { + try + { + Directory.Move(source, destination); + } + catch (IOException) + { + // Cross device move requires a copy + Directory.CreateDirectory(destination); + foreach (string file in Directory.GetFiles(source)) + { + File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); + } + + Directory.Delete(source, true); + } + } +} diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 939376dd8..18d924aa8 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Events; using Jellyfin.Server.Implementations.Security; +using Jellyfin.Server.Implementations.Trickplay; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; using MediaBrowser.Controller.BaseItemManager; @@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Activity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -77,6 +79,7 @@ namespace Jellyfin.Server serviceCollection.AddSingleton(); serviceCollection.AddScoped(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); // TODO search the assemblies instead of adding them manually? serviceCollection.AddSingleton(); diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 42c680761..c7bfbdb53 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Drawing; namespace MediaBrowser.Controller.Drawing @@ -84,13 +83,13 @@ namespace MediaBrowser.Controller.Drawing void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops); /// - /// Creates a new jpeg trickplay grid image. + /// Creates a new trickplay tile image. /// - /// The options to use when creating the image. Width and Height are a quantity of tiles in this case, not pixels. + /// The options to use when creating the image. Width and Height are a quantity of thumbnails in this case, not pixels. /// The image encode quality. - /// The width of a single trickplay image. - /// Optional height of a single trickplay image, if it is known. - /// Height of single decoded trickplay image. - int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight); + /// The width of a single trickplay thumbnail. + /// Optional height of a single trickplay thumbnail, if it is known. + /// Height of single decoded trickplay thumbnail. + int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight); } } diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 11eb4932c..2c52b2b45 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -61,27 +61,6 @@ namespace MediaBrowser.Controller.Persistence /// The list of chapters to save. void SaveChapters(Guid id, IReadOnlyList chapters); - /// - /// Get available trickplay resolutions and corresponding info. - /// - /// The item. - /// Map of width resolutions to trickplay tiles info. - Dictionary GetTilesResolutions(Guid itemId); - - /// - /// Saves trickplay tiles info. - /// - /// The item. - /// The trickplay tiles info. - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); - - /// - /// Gets trickplay data for an item. - /// - /// The item. - /// A map of media source id to a map of tile width to tile info. - Dictionary> GetTrickplayManifest(BaseItem item); - /// /// Gets the media streams. /// diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 8d36fc3ff..0a1e780b2 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Trickplay; @@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.Trickplay; public interface ITrickplayManager { /// - /// Generate or replace trickplay data. + /// Generates new trickplay images and metadata. /// /// The video. /// Whether or not existing data should be replaced. @@ -26,28 +26,28 @@ public interface ITrickplayManager /// /// The item. /// Map of width resolutions to trickplay tiles info. - Dictionary GetTilesResolutions(Guid itemId); + Task> GetTrickplayResolutions(Guid itemId); /// - /// Saves trickplay tiles info. + /// Saves trickplay info. /// - /// The item. - /// The trickplay tiles info. - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + /// The trickplay info. + /// Task. + Task SaveTrickplayInfo(TrickplayInfo info); /// - /// Gets the trickplay manifest. + /// Gets all trickplay infos for all media streams of an item. /// /// The item. - /// A map of media source id to a map of tile width to tile info. - Dictionary> GetTrickplayManifest(BaseItem item); + /// A map of media source id to a map of tile width to trickplay info. + Task>> GetTrickplayManifest(BaseItem item); /// - /// Gets the path to a trickplay tiles image. + /// Gets the path to a trickplay tile image. /// /// The item. - /// The width of a single tile. - /// The tile grid's index. + /// The width of a single thumbnail. + /// The tile's index. /// The absolute path. string GetTrickplayTilePath(BaseItem item, int width, int index); @@ -55,8 +55,8 @@ public interface ITrickplayManager /// Gets the trickplay HLS playlist. /// /// The item. - /// The width of a single tile. + /// The width of a single thumbnail. /// Optional api key of the requesting user. /// The text content of the .m3u8 playlist. - string? GetHlsPlaylist(Guid itemId, int width, string? apiKey); + Task GetHlsPlaylist(Guid itemId, int width, string? apiKey); } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 3db9cb08b..287966dd0 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; @@ -572,7 +573,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the trickplay manifest. /// /// The trickplay manifest. - public Dictionary> Trickplay { get; set; } + public Dictionary> Trickplay { get; set; } /// /// Gets or sets the type of the location. diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs deleted file mode 100644 index 86d37787f..000000000 --- a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace MediaBrowser.Model.Entities; - -/// -/// Class TrickplayTilesInfo. -/// -public class TrickplayTilesInfo -{ - /// - /// Gets or sets width of an individual tile. - /// - /// The width. - public int Width { get; set; } - - /// - /// Gets or sets height of an individual tile. - /// - /// The height. - public int Height { get; set; } - - /// - /// Gets or sets amount of tiles per row. - /// - /// The tile grid's width. - public int TileWidth { get; set; } - - /// - /// Gets or sets amount of tiles per column. - /// - /// The tile grid's height. - public int TileHeight { get; set; } - - /// - /// Gets or sets total amount of non-black tiles. - /// - /// The tile count. - public int TileCount { get; set; } - - /// - /// Gets or sets interval in milliseconds between each trickplay tile. - /// - /// The interval. - public int Interval { get; set; } - - /// - /// Gets or sets peak bandwith usage in bits per second. - /// - /// The bandwidth. - public int Bandwidth { get; set; } -} diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs deleted file mode 100644 index 3ea7c00d0..000000000 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ /dev/null @@ -1,425 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Trickplay; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.Trickplay; - -/// -/// ITrickplayManager implementation. -/// -public class TrickplayManager : ITrickplayManager -{ - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; - private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly EncodingHelper _encodingHelper; - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; - private readonly IImageEncoder _imageEncoder; - - private static readonly SemaphoreSlim _resourcePool = new(1, 1); - private static readonly string[] _trickplayImgExtensions = { ".jpg" }; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The item repository. - /// The media encoder. - /// The file systen. - /// The encoding helper. - /// The library manager. - /// The server configuration manager. - /// The image encoder. - public TrickplayManager( - ILogger logger, - IItemRepository itemRepo, - IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - EncodingHelper encodingHelper, - ILibraryManager libraryManager, - IServerConfigurationManager config, - IImageEncoder imageEncoder) - { - _logger = logger; - _itemRepo = itemRepo; - _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _encodingHelper = encodingHelper; - _libraryManager = libraryManager; - _config = config; - _imageEncoder = imageEncoder; - } - - /// - public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken) - { - _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); - - var options = _config.Configuration.TrickplayOptions; - foreach (var width in options.WidthResolutions) - { - cancellationToken.ThrowIfCancellationRequested(); - await RefreshTrickplayDataInternal( - video, - replace, - width, - options, - cancellationToken).ConfigureAwait(false); - } - } - - private async Task RefreshTrickplayDataInternal( - Video video, - bool replace, - int width, - TrickplayOptions options, - CancellationToken cancellationToken) - { - if (!CanGenerateTrickplay(video, options.Interval)) - { - return; - } - - var imgTempDir = string.Empty; - var outputDir = GetTrickplayDirectory(video, width); - - await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) - { - _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); - return; - } - - // Extract images - // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. - var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); - - if (mediaSource is null) - { - _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); - return; - } - - var mediaPath = mediaSource.Path; - var mediaStream = mediaSource.VideoStream; - var container = mediaSource.Container; - - _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); - imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( - mediaPath, - container, - mediaSource, - mediaStream, - width, - TimeSpan.FromMilliseconds(options.Interval), - options.EnableHwAcceleration, - options.ProcessThreads, - options.Qscale, - options.ProcessPriority, - _encodingHelper, - cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) - { - throw new InvalidOperationException("Null or invalid directory from media encoder."); - } - - var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false) - .Select(i => i.FullName) - .OrderBy(i => i) - .ToList(); - - // Create tiles - var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); - - // Save tiles info - try - { - if (tilesInfo is not null) - { - SaveTilesInfo(video.Id, tilesInfo); - _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); - } - else - { - throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while saving trickplay tiles info."); - - // Make sure no files stay in metadata folders on failure - // if tiles info wasn't saved. - Directory.Delete(outputDir, true); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating trickplay images."); - } - finally - { - _resourcePool.Release(); - - if (!string.IsNullOrEmpty(imgTempDir)) - { - Directory.Delete(imgTempDir, true); - } - } - } - - private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir) - { - if (images.Count == 0) - { - throw new ArgumentException("Can't create trickplay from 0 images."); - } - - Directory.CreateDirectory(workDir); - - var tilesInfo = new TrickplayTilesInfo - { - Width = width, - Interval = options.Interval, - TileWidth = options.TileWidth, - TileHeight = options.TileHeight, - TileCount = images.Count, - // Set during image generation - Height = 0, - Bandwidth = 0 - }; - - /* - * Generate trickplay tile grids from sets of images - */ - var imageOptions = new ImageCollageOptions - { - Width = tilesInfo.TileWidth, - Height = tilesInfo.TileHeight - }; - - var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var requiredTileGrids = (int)Math.Ceiling((double)images.Count / tilesPerGrid); - - for (int i = 0; i < requiredTileGrids; i++) - { - // Set output/input paths - var tileGridPath = Path.Combine(workDir, $"{i}.jpg"); - - imageOptions.OutputPath = tileGridPath; - imageOptions.InputPaths = images.Skip(i * tilesPerGrid).Take(tilesPerGrid).ToList(); - - // Generate image and use returned height for tiles info - var height = _imageEncoder.CreateTrickplayGrid(imageOptions, options.JpegQuality, tilesInfo.Width, tilesInfo.Height != 0 ? tilesInfo.Height : null); - if (tilesInfo.Height == 0) - { - tilesInfo.Height = height; - } - - // Update bitrate - var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); - tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); - } - - /* - * Move trickplay tiles to output directory - */ - Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); - - // Replace existing tile grids if they already exist - if (Directory.Exists(outputDir)) - { - Directory.Delete(outputDir, true); - } - - MoveDirectory(workDir, outputDir); - - return tilesInfo; - } - - private bool CanGenerateTrickplay(Video video, int interval) - { - var videoType = video.VideoType; - if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) - { - return false; - } - - if (video.IsPlaceHolder) - { - return false; - } - - if (video.IsShortcut) - { - return false; - } - - if (!video.IsCompleteMedia) - { - return false; - } - - if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) - { - return false; - } - - var libraryOptions = _libraryManager.GetLibraryOptions(video); - if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction) - { - return false; - } - - // Can't extract images if there are no video streams - return video.GetMediaStreams().Count > 0; - } - - /// - public Dictionary GetTilesResolutions(Guid itemId) - { - return _itemRepo.GetTilesResolutions(itemId); - } - - /// - public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) - { - _itemRepo.SaveTilesInfo(itemId, tilesInfo); - } - - /// - public Dictionary> GetTrickplayManifest(BaseItem item) - { - return _itemRepo.GetTrickplayManifest(item); - } - - /// - public string GetTrickplayTilePath(BaseItem item, int width, int index) - { - return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); - } - - /// - public string? GetHlsPlaylist(Guid itemId, int width, string? apiKey) - { - var tilesResolutions = GetTilesResolutions(itemId); - if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo)) - { - var builder = new StringBuilder(128); - - if (tilesInfo.TileCount > 0) - { - const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; - const string decimalFormat = "{0:0.###}"; - - var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}"; - var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}"; - var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var tileDuration = tilesInfo.Interval / 1000d; - var infDuration = tileDuration * tilesPerGrid; - var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); - - builder - .AppendLine("#EXTM3U") - .Append("#EXT-X-TARGETDURATION:") - .AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)) - .AppendLine("#EXT-X-VERSION:7") - .AppendLine("#EXT-X-MEDIA-SEQUENCE:1") - .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") - .AppendLine("#EXT-X-IMAGES-ONLY"); - - for (int i = 0; i < tileGridCount; i++) - { - // All tile grids before the last one must contain full amount of tiles. - // The final grid will be 0 < count <= maxTiles - if (i == tileGridCount - 1) - { - tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); - infDuration = tileDuration * tilesPerGrid; - } - - // EXTINF - builder - .Append("#EXTINF:") - .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration) - .AppendLine(","); - - // EXT-X-TILES - builder - .Append("#EXT-X-TILES:RESOLUTION=") - .Append(resolution) - .Append(",LAYOUT=") - .Append(layout) - .Append(",DURATION=") - .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration) - .AppendLine(); - - // URL - builder - .AppendFormat( - CultureInfo.InvariantCulture, - urlFormat, - width.ToString(CultureInfo.InvariantCulture), - i.ToString(CultureInfo.InvariantCulture), - itemId.ToString("N"), - apiKey) - .AppendLine(); - } - - builder.AppendLine("#EXT-X-ENDLIST"); - return builder.ToString(); - } - } - - return null; - } - - private string GetTrickplayDirectory(BaseItem item, int? width = null) - { - var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); - - return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; - } - - private void MoveDirectory(string source, string destination) - { - try - { - Directory.Move(source, destination); - } - catch (IOException) - { - // Cross device move requires a copy - Directory.CreateDirectory(destination); - foreach (string file in Directory.GetFiles(source)) - { - File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); - } - - Directory.Delete(source, true); - } - } -} diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2facf0f37..b387c437b 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -531,7 +531,7 @@ public class SkiaEncoder : IImageEncoder } /// - public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) { var paths = options.InputPaths; var tileWidth = options.Width; diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs index 15345e1bc..1495661c1 100644 --- a/src/Jellyfin.Drawing/NullImageEncoder.cs +++ b/src/Jellyfin.Drawing/NullImageEncoder.cs @@ -50,7 +50,7 @@ public class NullImageEncoder : IImageEncoder } /// - public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) { throw new NotImplementedException(); } -- cgit v1.2.3 From fc619337481e3bb0f686aee06d9c5630926baaef Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Mon, 26 Jun 2023 22:04:39 -0700 Subject: Minor code changes --- Emby.Server.Implementations/Dto/DtoService.cs | 2 +- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'MediaBrowser.Model') diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 933b95df0..19ae39662 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1064,7 +1064,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Trickplay)) { - var manifest = _trickplayManager.GetTrickplayManifest(item).ConfigureAwait(false).GetAwaiter().GetResult(); + var manifest = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); // To stay consistent with other fields, this must go from a Guid to a non-dashed string. // This does not seem to occur automatically to dictionaries like it does with other Guid fields. diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 097eff295..18f107503 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -265,6 +265,10 @@ namespace MediaBrowser.Model.Configuration /// The limit for parallel image encoding. public int ParallelImageEncodingLimit { get; set; } + /// + /// Gets or sets the trickplay options. + /// + /// The trickplay options. public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions(); } } -- cgit v1.2.3 From ba7e3bfd82aeee3c74a99fa1f4ca94deca8b4dbe Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sat, 23 Sep 2023 10:59:13 -0600 Subject: Migrate to customizable cast receiver config --- .config/dotnet-tools.json | 12 + Emby.Server.Implementations/ApplicationHost.cs | 3 +- Jellyfin.Data/Entities/User.cs | 6 + .../20230923170422_UserCastReceiver.Designer.cs | 654 +++++++++++++++++++++ .../Migrations/20230923170422_UserCastReceiver.cs | 29 + .../Migrations/JellyfinDbModelSnapshot.cs | 6 +- .../Users/UserManager.cs | 16 +- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../Migrations/Routines/AddDefaultCastReceivers.cs | 55 ++ .../Configuration/ServerConfiguration.cs | 497 ++++++++-------- .../Configuration/UserConfiguration.cs | 5 + .../System/CastReceiverApplication.cs | 17 + MediaBrowser.Model/System/SystemInfo.cs | 6 + 13 files changed, 1058 insertions(+), 251 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs create mode 100644 Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs create mode 100644 MediaBrowser.Model/System/CastReceiverApplication.cs (limited to 'MediaBrowser.Model') diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 000000000..96be437d5 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "7.0.11", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f8208af70..5f8d275b6 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -968,7 +968,8 @@ namespace Emby.Server.Implementations ServerName = FriendlyName, LocalAddress = GetSmartApiUrl(request), SupportsLibraryMonitor = true, - PackageName = _startupOptions.PackageName + PackageName = _startupOptions.PackageName, + CastReceiverApplications = ConfigurationManager.Configuration.CastReceiverApplications }; } diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 58ddaaf83..5c3e0338d 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities /// public SyncPlayUserAccessType SyncPlayAccess { get; set; } + /// + /// Gets or sets the cast receiver id. + /// + [StringLength(32)] + public string? CastReceiverId { get; set; } + /// [ConcurrencyCheck] public uint RowVersion { get; private set; } diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs new file mode 100644 index 000000000..2884d4256 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs @@ -0,0 +1,654 @@ +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20230923170422_UserCastReceiver")] + partial class UserCastReceiver + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs new file mode 100644 index 000000000..f06410c15 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class UserCastReceiver : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CastReceiverId", + table: "Users", + type: "TEXT", + maxLength: 32, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CastReceiverId", + table: "Users"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index d23508096..6cd4594f6 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -457,6 +457,10 @@ namespace Jellyfin.Server.Implementations.Migrations .HasMaxLength(255) .HasColumnType("TEXT"); + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + b.Property("DisplayCollectionsView") .HasColumnType("INTEGER"); diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 5010751dd..2a38fcecc 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -15,6 +15,7 @@ using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; @@ -45,6 +46,7 @@ namespace Jellyfin.Server.Implementations.Users private readonly InvalidAuthProvider _invalidAuthProvider; private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; + private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IDictionary _users; @@ -58,6 +60,7 @@ namespace Jellyfin.Server.Implementations.Users /// The application host. /// The image processor. /// The logger. + /// The system config manager. public UserManager( IDbContextFactory dbProvider, IEventManager eventManager, @@ -65,7 +68,8 @@ namespace Jellyfin.Server.Implementations.Users INetworkManager networkManager, IApplicationHost appHost, IImageProcessor imageProcessor, - ILogger logger) + ILogger logger, + IServerConfigurationManager serverConfigurationManager) { _dbProvider = dbProvider; _eventManager = eventManager; @@ -74,6 +78,7 @@ namespace Jellyfin.Server.Implementations.Users _appHost = appHost; _imageProcessor = imageProcessor; _logger = logger; + _serverConfigurationManager = serverConfigurationManager; _passwordResetProviders = appHost.GetExports(); _authenticationProviders = appHost.GetExports(); @@ -320,7 +325,10 @@ namespace Jellyfin.Server.Implementations.Users OrderedViews = user.GetPreferenceValues(PreferenceKind.OrderedViews), GroupedFolders = user.GetPreferenceValues(PreferenceKind.GroupedFolders), MyMediaExcludes = user.GetPreferenceValues(PreferenceKind.MyMediaExcludes), - LatestItemsExcludes = user.GetPreferenceValues(PreferenceKind.LatestItemExcludes) + LatestItemsExcludes = user.GetPreferenceValues(PreferenceKind.LatestItemExcludes), + CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId) + ? _serverConfigurationManager.Configuration.CastReceiverApplications.FirstOrDefault()?.Id + : user.CastReceiverId }, Policy = new UserPolicy { @@ -608,6 +616,10 @@ namespace Jellyfin.Server.Implementations.Users user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; user.RememberSubtitleSelections = config.RememberSubtitleSelections; user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; + if (!string.IsNullOrEmpty(config.CastReceiverId)) + { + user.CastReceiverId = config.CastReceiverId; + } user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 2db0b77cd..757b56a49 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -42,7 +42,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.RemoveDownloadImagesInAdvance), typeof(Routines.MigrateAuthenticationDb), typeof(Routines.FixPlaylistOwner), - typeof(Routines.MigrateRatingLevels) + typeof(Routines.MigrateRatingLevels), + typeof(Routines.AddDefaultCastReceivers) }; /// diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs new file mode 100644 index 000000000..75a6a6176 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs @@ -0,0 +1,55 @@ +using System; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.System; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// Migration to add the default cast receivers to the system config. +/// +public class AddDefaultCastReceivers : IMigrationRoutine +{ + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public AddDefaultCastReceivers(IServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + } + + /// + public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8"); + + /// + public string Name => "AddDefaultCastReceivers"; + + /// + public bool PerformOnNewInstall => true; + + /// + public void Perform() + { + // Only add if receiver list is empty. + if (_serverConfigurationManager.Configuration.CastReceiverApplications.Length == 0) + { + _serverConfigurationManager.Configuration.CastReceiverApplications = new CastReceiverApplication[] + { + new() + { + Id = "F007D354", + Name = "Stable" + }, + new() + { + Id = "6F511C87", + Name = "Unstable" + } + }; + + _serverConfigurationManager.SaveConfiguration(); + } + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 78a310f0b..1342ffb48 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -4,265 +4,270 @@ using System; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; +using MediaBrowser.Model.System; using MediaBrowser.Model.Updates; -namespace MediaBrowser.Model.Configuration +namespace MediaBrowser.Model.Configuration; + +/// +/// Represents the server configuration. +/// +public class ServerConfiguration : BaseApplicationConfiguration { /// - /// Represents the server configuration. + /// Initializes a new instance of the class. /// - public class ServerConfiguration : BaseApplicationConfiguration + public ServerConfiguration() { - /// - /// Initializes a new instance of the class. - /// - public ServerConfiguration() + MetadataOptions = new[] { - MetadataOptions = new[] + new MetadataOptions() + { + ItemType = "Book" + }, + new MetadataOptions() + { + ItemType = "Movie" + }, + new MetadataOptions + { + ItemType = "MusicVideo", + DisabledMetadataFetchers = new[] { "The Open Movie Database" }, + DisabledImageFetchers = new[] { "The Open Movie Database" } + }, + new MetadataOptions + { + ItemType = "Series", + }, + new MetadataOptions + { + ItemType = "MusicAlbum", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "MusicArtist", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions { - new MetadataOptions() - { - ItemType = "Book" - }, - new MetadataOptions() - { - ItemType = "Movie" - }, - new MetadataOptions - { - ItemType = "MusicVideo", - DisabledMetadataFetchers = new[] { "The Open Movie Database" }, - DisabledImageFetchers = new[] { "The Open Movie Database" } - }, - new MetadataOptions - { - ItemType = "Series", - }, - new MetadataOptions - { - ItemType = "MusicAlbum", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "MusicArtist", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "BoxSet" - }, - new MetadataOptions - { - ItemType = "Season", - }, - new MetadataOptions - { - ItemType = "Episode", - } - }; - } - - /// - /// Gets or sets a value indicating whether to enable prometheus metrics exporting. - /// - public bool EnableMetrics { get; set; } = false; - - public bool EnableNormalizedItemByNameIds { get; set; } = true; - - /// - /// Gets or sets a value indicating whether this instance is port authorized. - /// - /// true if this instance is port authorized; otherwise, false. - public bool IsPortAuthorized { get; set; } - - /// - /// Gets or sets a value indicating whether quick connect is available for use on this server. - /// - public bool QuickConnectAvailable { get; set; } = true; - - /// - /// Gets or sets a value indicating whether [enable case sensitive item ids]. - /// - /// true if [enable case sensitive item ids]; otherwise, false. - public bool EnableCaseSensitiveItemIds { get; set; } = true; - - public bool DisableLiveTvChannelUserDataName { get; set; } = true; - - /// - /// Gets or sets the metadata path. - /// - /// The metadata path. - public string MetadataPath { get; set; } = string.Empty; - - public string MetadataNetworkPath { get; set; } = string.Empty; - - /// - /// Gets or sets the preferred metadata language. - /// - /// The preferred metadata language. - public string PreferredMetadataLanguage { get; set; } = "en"; - - /// - /// Gets or sets the metadata country code. - /// - /// The metadata country code. - public string MetadataCountryCode { get; set; } = "US"; - - /// - /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name. - /// - /// The sort replace characters. - public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" }; - - /// - /// Gets or sets characters to be removed from strings to create a sort name. - /// - /// The sort remove characters. - public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; - - /// - /// Gets or sets words to be removed from strings to create a sort name. - /// - /// The sort remove words. - public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; - - /// - /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. - /// - /// The min resume PCT. - public int MinResumePct { get; set; } = 5; - - /// - /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. - /// - /// The max resume PCT. - public int MaxResumePct { get; set; } = 90; - - /// - /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates.. - /// - /// The min resume duration seconds. - public int MinResumeDurationSeconds { get; set; } = 300; - - /// - /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated. - /// - /// The min resume in minutes. - public int MinAudiobookResume { get; set; } = 5; - - /// - /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. - /// - /// The remaining time in minutes. - public int MaxAudiobookResume { get; set; } = 5; - - /// - /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed - /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several - /// different directories and files. - /// - /// The file watcher delay. - public int LibraryMonitorDelay { get; set; } = 60; - - /// - /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification. - /// - /// The library update duration. - public int LibraryUpdateDuration { get; set; } = 30; - - /// - /// Gets or sets the image saving convention. - /// - /// The image saving convention. - public ImageSavingConvention ImageSavingConvention { get; set; } - - public MetadataOptions[] MetadataOptions { get; set; } - - public bool SkipDeserializationForBasicTypes { get; set; } = true; - - public string ServerName { get; set; } = string.Empty; - - public string UICulture { get; set; } = "en-US"; - - public bool SaveMetadataHidden { get; set; } = false; - - public NameValuePair[] ContentTypes { get; set; } = Array.Empty(); - - public int RemoteClientBitrateLimit { get; set; } - - public bool EnableFolderView { get; set; } = false; - - public bool EnableGroupingIntoCollections { get; set; } = false; - - public bool DisplaySpecialsWithinSeasons { get; set; } = true; - - public string[] CodecsUsed { get; set; } = Array.Empty(); - - public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty(); - - public bool EnableExternalContentInSuggestions { get; set; } = true; - - public int ImageExtractionTimeoutMs { get; set; } - - public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty(); - - /// - /// Gets or sets a value indicating whether slow server responses should be logged as a warning. - /// - public bool EnableSlowResponseWarning { get; set; } = true; - - /// - /// Gets or sets the threshold for the slow response time warning in ms. - /// - public long SlowResponseThresholdMs { get; set; } = 500; - - /// - /// Gets or sets the cors hosts. - /// - public string[] CorsHosts { get; set; } = new[] { "*" }; - - /// - /// Gets or sets the number of days we should retain activity logs. - /// - public int? ActivityLogRetentionDays { get; set; } = 30; + ItemType = "BoxSet" + }, + new MetadataOptions + { + ItemType = "Season", + }, + new MetadataOptions + { + ItemType = "Episode", + } + }; + } + + /// + /// Gets or sets a value indicating whether to enable prometheus metrics exporting. + /// + public bool EnableMetrics { get; set; } = false; + + public bool EnableNormalizedItemByNameIds { get; set; } = true; - /// - /// Gets or sets the how the library scan fans out. - /// - public int LibraryScanFanoutConcurrency { get; set; } + /// + /// Gets or sets a value indicating whether this instance is port authorized. + /// + /// true if this instance is port authorized; otherwise, false. + public bool IsPortAuthorized { get; set; } - /// - /// Gets or sets the how many metadata refreshes can run concurrently. - /// - public int LibraryMetadataRefreshConcurrency { get; set; } + /// + /// Gets or sets a value indicating whether quick connect is available for use on this server. + /// + public bool QuickConnectAvailable { get; set; } = true; + + /// + /// Gets or sets a value indicating whether [enable case sensitive item ids]. + /// + /// true if [enable case sensitive item ids]; otherwise, false. + public bool EnableCaseSensitiveItemIds { get; set; } = true; - /// - /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder. - /// - public bool RemoveOldPlugins { get; set; } + public bool DisableLiveTvChannelUserDataName { get; set; } = true; - /// - /// Gets or sets a value indicating whether clients should be allowed to upload logs. - /// - public bool AllowClientLogUpload { get; set; } = true; + /// + /// Gets or sets the metadata path. + /// + /// The metadata path. + public string MetadataPath { get; set; } = string.Empty; - /// - /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether. - /// - /// The dummy chapters duration. - public int DummyChapterDuration { get; set; } + public string MetadataNetworkPath { get; set; } = string.Empty; - /// - /// Gets or sets the chapter image resolution. - /// - /// The chapter image resolution. - public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource; + /// + /// Gets or sets the preferred metadata language. + /// + /// The preferred metadata language. + public string PreferredMetadataLanguage { get; set; } = "en"; - /// - /// Gets or sets the limit for parallel image encoding. - /// - /// The limit for parallel image encoding. - public int ParallelImageEncodingLimit { get; set; } - } + /// + /// Gets or sets the metadata country code. + /// + /// The metadata country code. + public string MetadataCountryCode { get; set; } = "US"; + + /// + /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name. + /// + /// The sort replace characters. + public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" }; + + /// + /// Gets or sets characters to be removed from strings to create a sort name. + /// + /// The sort remove characters. + public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; + + /// + /// Gets or sets words to be removed from strings to create a sort name. + /// + /// The sort remove words. + public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; + + /// + /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. + /// + /// The min resume PCT. + public int MinResumePct { get; set; } = 5; + + /// + /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. + /// + /// The max resume PCT. + public int MaxResumePct { get; set; } = 90; + + /// + /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates.. + /// + /// The min resume duration seconds. + public int MinResumeDurationSeconds { get; set; } = 300; + + /// + /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated. + /// + /// The min resume in minutes. + public int MinAudiobookResume { get; set; } = 5; + + /// + /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. + /// + /// The remaining time in minutes. + public int MaxAudiobookResume { get; set; } = 5; + + /// + /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed + /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several + /// different directories and files. + /// + /// The file watcher delay. + public int LibraryMonitorDelay { get; set; } = 60; + + /// + /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification. + /// + /// The library update duration. + public int LibraryUpdateDuration { get; set; } = 30; + + /// + /// Gets or sets the image saving convention. + /// + /// The image saving convention. + public ImageSavingConvention ImageSavingConvention { get; set; } + + public MetadataOptions[] MetadataOptions { get; set; } + + public bool SkipDeserializationForBasicTypes { get; set; } = true; + + public string ServerName { get; set; } = string.Empty; + + public string UICulture { get; set; } = "en-US"; + + public bool SaveMetadataHidden { get; set; } = false; + + public NameValuePair[] ContentTypes { get; set; } = Array.Empty(); + + public int RemoteClientBitrateLimit { get; set; } + + public bool EnableFolderView { get; set; } = false; + + public bool EnableGroupingIntoCollections { get; set; } = false; + + public bool DisplaySpecialsWithinSeasons { get; set; } = true; + + public string[] CodecsUsed { get; set; } = Array.Empty(); + + public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty(); + + public bool EnableExternalContentInSuggestions { get; set; } = true; + + public int ImageExtractionTimeoutMs { get; set; } + + public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether slow server responses should be logged as a warning. + /// + public bool EnableSlowResponseWarning { get; set; } = true; + + /// + /// Gets or sets the threshold for the slow response time warning in ms. + /// + public long SlowResponseThresholdMs { get; set; } = 500; + + /// + /// Gets or sets the cors hosts. + /// + public string[] CorsHosts { get; set; } = new[] { "*" }; + + /// + /// Gets or sets the number of days we should retain activity logs. + /// + public int? ActivityLogRetentionDays { get; set; } = 30; + + /// + /// Gets or sets the how the library scan fans out. + /// + public int LibraryScanFanoutConcurrency { get; set; } + + /// + /// Gets or sets the how many metadata refreshes can run concurrently. + /// + public int LibraryMetadataRefreshConcurrency { get; set; } + + /// + /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder. + /// + public bool RemoveOldPlugins { get; set; } + + /// + /// Gets or sets a value indicating whether clients should be allowed to upload logs. + /// + public bool AllowClientLogUpload { get; set; } = true; + + /// + /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether. + /// + /// The dummy chapters duration. + public int DummyChapterDuration { get; set; } + + /// + /// Gets or sets the chapter image resolution. + /// + /// 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; } + + /// + /// Gets or sets the list of cast receiver applications. + /// + public CastReceiverApplication[] CastReceiverApplications { get; set; } = Array.Empty(); } diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index 94f354660..b477f2593 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -69,5 +69,10 @@ namespace MediaBrowser.Model.Configuration public bool RememberSubtitleSelections { get; set; } public bool EnableNextEpisodeAutoPlay { get; set; } + + /// + /// Gets or sets the id of the selected cast receiver. + /// + public string? CastReceiverId { get; set; } } } diff --git a/MediaBrowser.Model/System/CastReceiverApplication.cs b/MediaBrowser.Model/System/CastReceiverApplication.cs new file mode 100644 index 000000000..6a49a5cac --- /dev/null +++ b/MediaBrowser.Model/System/CastReceiverApplication.cs @@ -0,0 +1,17 @@ +namespace MediaBrowser.Model.System; + +/// +/// The cast receiver application model. +/// +public class CastReceiverApplication +{ + /// + /// Gets or sets the cast receiver application id. + /// + public required string Id { get; set; } + + /// + /// Gets or sets the cast receiver application name. + /// + public required string Name { get; set; } +} diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index bd0099af7..d9544b40f 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using MediaBrowser.Model.Updates; @@ -128,6 +129,11 @@ namespace MediaBrowser.Model.System /// The transcode path. public string TranscodingTempPath { get; set; } + /// + /// Gets or sets the list of cast receiver applications. + /// + public IReadOnlyList CastReceiverApplications { get; set; } + /// /// Gets or sets a value indicating whether this instance has update available. /// -- cgit v1.2.3 From 47254d6a2236e079af3cc8c2e37c77d9d1479235 Mon Sep 17 00:00:00 2001 From: Stepan Goremykin Date: Sun, 8 Oct 2023 00:46:15 +0200 Subject: Remove conditional access when it is known to be not null --- Emby.Server.Implementations/Net/SocketFactory.cs | 6 +++--- Jellyfin.Api/Controllers/LibraryController.cs | 6 +++--- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 3091398b7..8478fbc4a 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Net } catch { - socket?.Dispose(); + socket.Dispose(); throw; } @@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Net } catch { - socket?.Dispose(); + socket.Dispose(); throw; } @@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Net } catch { - socket?.Dispose(); + socket.Dispose(); throw; } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 46c0a8d52..21941ff94 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController return new AllThemeMediaResult { - ThemeSongsResult = themeSongs?.Value, - ThemeVideosResult = themeVideos?.Value, + ThemeSongsResult = themeSongs.Value, + ThemeVideosResult = themeVideos.Value, SoundtrackSongsResult = new ThemeMediaResult() }; } @@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); - parent = parent?.GetParent(); + parent = parent.GetParent(); } return baseItemDtos; diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 889e2494a..666e78795 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1313,7 +1313,7 @@ namespace MediaBrowser.Model.Dlna var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream)); var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions); - if (audioStream?.IsExternal == true) + if (audioStream.IsExternal == true) { audioStreamFailureReasons |= TranscodeReason.AudioIsExternal; } -- cgit v1.2.3 From c707baed8366cd44ea80713cf93e5f92971815bf Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 5 Oct 2023 23:57:11 +0200 Subject: Jellyfin.Drawing minor improvements Reduce duplicate/dead code --- MediaBrowser.Controller/Drawing/IImageProcessor.cs | 11 ----- .../Drawing/ImageProcessingOptions.cs | 3 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 10 +--- .../Drawing/ImageFormatExtensions.cs | 17 +++++++ .../MediaInfo/EmbeddedImageProvider.cs | 6 --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 14 +----- src/Jellyfin.Drawing/ImageProcessor.cs | 56 +--------------------- .../Drawing/ImageFormatExtensionsTests.cs | 13 +++++ 8 files changed, 37 insertions(+), 93 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index cdc3d52b9..0d1e2a5a0 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; @@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing string? GetImageCacheTag(User user); - /// - /// Processes the image. - /// - /// The options. - /// To stream. - /// Task. - Task ProcessImage(ImageProcessingOptions options, Stream toStream); - /// /// Processes the image. /// @@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing /// The options. /// The library name to draw onto the collage. void CreateImageCollage(ImageCollageOptions options, string? libraryName); - - bool SupportsTransparency(string path); } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 7912c5e87..953cfe698 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing private bool IsFormatSupported(string originalImagePath) { var ext = Path.GetExtension(originalImagePath); - return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase)); + ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase); + return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase)); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4bff19665..26f47a18f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -650,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { ArgumentException.ThrowIfNullOrEmpty(inputPath); - var outputExtension = targetFormat switch - { - ImageFormat.Bmp => ".bmp", - ImageFormat.Gif => ".gif", - ImageFormat.Jpg => ".jpg", - ImageFormat.Png => ".png", - ImageFormat.Webp => ".webp", - _ => ".jpg" - }; + var outputExtension = targetFormat?.GetExtension() ?? ".jpg"; var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs index 68a5c2534..1bb24112e 100644 --- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs +++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs @@ -24,4 +24,21 @@ public static class ImageFormatExtensions ImageFormat.Webp => "image/webp", _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) }; + + /// + /// Returns the correct extension for this . + /// + /// This . + /// The is an invalid enumeration value. + /// The correct extension for this . + public static string GetExtension(this ImageFormat format) + => format switch + { + ImageFormat.Bmp => ".bmp", + ImageFormat.Gif => ".gif", + ImageFormat.Jpg => ".jpg", + ImageFormat.Png => ".png", + ImageFormat.Webp => ".webp", + _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) + }; } diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index c24f4e2fc..0bfee07fd 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo ? Path.GetExtension(attachmentStream.FileName) : MimeTypes.ToExtension(attachmentStream.MimeType); - if (string.IsNullOrEmpty(extension)) - { - extension = ".jpg"; - } - ImageFormat format = extension switch { ".bmp" => ImageFormat.Bmp, ".gif" => ImageFormat.Gif, - ".jpg" => ImageFormat.Jpg, ".png" => ImageFormat.Png, ".webp" => ImageFormat.Webp, _ => ImageFormat.Jpg diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 416f60217..03f90da8e 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder { if (!orientation.HasValue) { - return SKEncodedOrigin.TopLeft; + return SKEncodedOrigin.Default; } - 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 - }; + return (SKEncodedOrigin)orientation.Value; } /// diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 4f16e294b..65a8f4e83 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable /// 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) { @@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable } } - return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); + return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); } catch (Exception ex) { @@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return ImageFormat.Jpg; } - 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. /// @@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable filename.Append(",v="); filename.Append(Version); - return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); + return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension()); } /// @@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return Task.FromResult((originalImagePath, dateModified)); } - // 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)); } diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs index a5bdb42d8..198ad5a27 100644 --- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs @@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests [InlineData((ImageFormat)5)] public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) => Assert.Throws(() => format.GetMimeType()); + + [Theory] + [MemberData(nameof(GetAllImageFormats))] + public static void GetExtension_Valid_Valid(ImageFormat format) + => Assert.Null(Record.Exception(() => format.GetExtension())); + + [Theory] + [InlineData((ImageFormat)int.MinValue)] + [InlineData((ImageFormat)int.MaxValue)] + [InlineData((ImageFormat)(-1))] + [InlineData((ImageFormat)5)] + public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) + => Assert.Throws(() => format.GetExtension()); } -- cgit v1.2.3 From 2920611ffc206d845563637c4a865bf3f02d1374 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sat, 13 May 2023 12:44:31 -0600 Subject: Convert string MediaType to enum MediaType --- Emby.Dlna/ContentDirectory/ControlHandler.cs | 2 +- Emby.Dlna/Didl/DidlBuilder.cs | 24 ++++++++-------- Emby.Dlna/PlayTo/PlayToController.cs | 7 +++-- Emby.Dlna/PlayTo/uBaseObject.cs | 13 +++++---- .../Data/SqliteItemRepository.cs | 16 ++++------- .../Library/MediaSourceManager.cs | 15 +++++----- .../Library/Resolvers/PlaylistResolver.cs | 1 + .../Library/UserViewManager.cs | 2 +- .../Library/Validators/CollectionPostScanTask.cs | 2 +- .../Playlists/PlaylistManager.cs | 15 +++++----- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 1 + Jellyfin.Api/Controllers/ArtistsController.cs | 4 +-- Jellyfin.Api/Controllers/FilterController.cs | 2 +- Jellyfin.Api/Controllers/ItemsController.cs | 6 ++-- Jellyfin.Api/Controllers/PlaylistsController.cs | 3 +- Jellyfin.Api/Controllers/SearchController.cs | 2 +- Jellyfin.Api/Controllers/SessionController.cs | 2 +- Jellyfin.Api/Controllers/SuggestionsController.cs | 2 +- Jellyfin.Api/Controllers/TrailersController.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 6 ++-- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 3 +- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 3 +- .../Models/SessionDtos/ClientCapabilitiesDto.cs | 3 +- Jellyfin.Data/Enums/MediaType.cs | 32 ++++++++++++++++++++++ .../Consumers/Session/PlaybackStartLogger.cs | 7 +++-- .../Events/Consumers/Session/PlaybackStopLogger.cs | 7 +++-- MediaBrowser.Controller/Entities/Audio/Audio.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- MediaBrowser.Controller/Entities/Book.cs | 2 +- .../Entities/InternalItemsQuery.cs | 4 +-- MediaBrowser.Controller/Entities/Photo.cs | 3 +- .../Entities/UserViewBuilder.cs | 2 +- MediaBrowser.Controller/Entities/Video.cs | 3 +- MediaBrowser.Controller/LiveTv/LiveTvChannel.cs | 2 +- MediaBrowser.Controller/Playlists/Playlist.cs | 14 +++++----- MediaBrowser.Controller/Session/SessionInfo.cs | 5 ++-- .../Parsers/PlaylistXmlParser.cs | 8 +++++- .../Savers/PlaylistXmlSaver.cs | 5 ++-- MediaBrowser.Model/Dlna/DeviceProfile.cs | 8 ++++-- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- MediaBrowser.Model/Entities/MediaType.cs | 28 ------------------- .../Playlists/PlaylistCreationRequest.cs | 3 +- MediaBrowser.Model/Search/SearchHint.cs | 4 +-- MediaBrowser.Model/Search/SearchQuery.cs | 4 +-- MediaBrowser.Model/Session/ClientCapabilities.cs | 5 ++-- .../MediaInfo/SubtitleScheduledTask.cs | 2 +- 47 files changed, 159 insertions(+), 133 deletions(-) create mode 100644 Jellyfin.Data/Enums/MediaType.cs delete mode 100644 MediaBrowser.Model/Entities/MediaType.cs (limited to 'MediaBrowser.Model') diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index abd594a3a..12cd7e2ef 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -494,7 +494,7 @@ namespace Emby.Dlna.ContentDirectory { var folder = (Folder)item; - string[] mediaTypes = Array.Empty(); + MediaType[] mediaTypes = Array.Empty(); bool? isFolder = null; switch (search.SearchType) diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 5ed982876..9f152df13 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -174,13 +174,14 @@ namespace Emby.Dlna.Didl if (item is IHasMediaSources) { - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + switch (item.MediaType) { - AddAudioResource(writer, item, deviceId, filter, streamInfo); - } - else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - AddVideoResource(writer, item, deviceId, filter, streamInfo); + case MediaType.Audio: + AddAudioResource(writer, item, deviceId, filter, streamInfo); + break; + case MediaType.Video: + AddVideoResource(writer, item, deviceId, filter, streamInfo); + break; } } @@ -821,15 +822,15 @@ namespace Emby.Dlna.Didl writer.WriteString(classType ?? "object.container.storageFolder"); } - else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Audio) { writer.WriteString("object.item.audioItem.musicTrack"); } - else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Photo) { writer.WriteString("object.item.imageItem.photo"); } - else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Video) { if (!_profile.RequiresPlainVideoItems && item is Movie) { @@ -1006,8 +1007,7 @@ namespace Emby.Dlna.Didl if (!_profile.EnableAlbumArtInDidl) { - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - || string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video) { if (!stubType.HasValue) { @@ -1016,7 +1016,7 @@ namespace Emby.Dlna.Didl } } - if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) + if (!_profile.EnableSingleAlbumArtLimit || item.MediaType == MediaType.Photo) { AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG"); AddImageResElement(item, writer, 1024, 768, "jpg", "JPEG_MED"); diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index b1ad15cdc..9e3d1d4d6 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.Didl; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; @@ -577,7 +578,7 @@ namespace Emby.Dlna.PlayTo private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) { - if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Video) { return new PlaylistItem { @@ -597,7 +598,7 @@ namespace Emby.Dlna.PlayTo }; } - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Audio) { return new PlaylistItem { @@ -615,7 +616,7 @@ namespace Emby.Dlna.PlayTo }; } - if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Photo) { return PlaylistItemFactory.Create((Photo)item, profile); } diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs index 2e0f2063b..a8f451405 100644 --- a/Emby.Dlna/PlayTo/uBaseObject.cs +++ b/Emby.Dlna/PlayTo/uBaseObject.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; namespace Emby.Dlna.PlayTo { @@ -33,19 +34,19 @@ namespace Emby.Dlna.PlayTo { var classType = UpnpClass ?? string.Empty; - if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Audio, StringComparison.Ordinal) != -1) + if (classType.Contains("Audio", StringComparison.Ordinal)) { - return MediaBrowser.Model.Entities.MediaType.Audio; + return "Audio"; } - if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1) + if (classType.Contains("Video", StringComparison.Ordinal)) { - return MediaBrowser.Model.Entities.MediaType.Video; + return "Video"; } - if (classType.IndexOf("image", StringComparison.Ordinal) != -1) + if (classType.Contains("image", StringComparison.Ordinal)) { - return MediaBrowser.Model.Entities.MediaType.Photo; + return "Photo"; } return null; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 77cf4089b..798521d22 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -722,7 +722,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@IsLocked", item.IsLocked); saveItemStatement.TryBind("@Name", item.Name); saveItemStatement.TryBind("@OfficialRating", item.OfficialRating); - saveItemStatement.TryBind("@MediaType", item.MediaType); + saveItemStatement.TryBind("@MediaType", item.MediaType.ToString()); saveItemStatement.TryBind("@Overview", item.Overview); saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber); saveItemStatement.TryBind("@PremiereDate", item.PremiereDate); @@ -3109,11 +3109,6 @@ namespace Emby.Server.Implementations.Data return true; } - private bool IsValidMediaType(string value) - { - return IsAlphaNumeric(value); - } - private bool IsValidPersonType(string value) { return IsAlphaNumeric(value); @@ -4124,15 +4119,14 @@ namespace Emby.Server.Implementations.Data } } - var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray(); - if (queryMediaTypes.Length == 1) + if (query.MediaTypes.Length == 1) { whereClauses.Add("MediaType=@MediaTypes"); - statement?.TryBind("@MediaTypes", queryMediaTypes[0]); + statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString()); } - else if (queryMediaTypes.Length > 1) + else if (query.MediaTypes.Length > 1) { - var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'")); + var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'")); whereClauses.Add("MediaType in (" + val + ")"); } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c9a26a30f..3d0f9f02c 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using EasyCaching.Core.Configurations; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; @@ -186,11 +187,11 @@ namespace Emby.Server.Implementations.Library { SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Audio) { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); } - else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Video) { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding); source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing); @@ -334,11 +335,11 @@ namespace Emby.Server.Implementations.Library { SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (item.MediaType == MediaType.Audio) { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); } - else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + else if (item.MediaType == MediaType.Video) { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding); source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing); @@ -417,9 +418,9 @@ namespace Emby.Server.Implementations.Library public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) { // Item would only be null if the app didn't supply ItemId as part of the live stream open request - var mediaType = item is null ? MediaType.Video : item.MediaType; + var mediaType = item?.MediaType ?? MediaType.Video; - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Video) { var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item); @@ -428,7 +429,7 @@ namespace Emby.Server.Implementations.Library SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection); SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); } - else if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + else if (mediaType == MediaType.Audio) { var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 5d569009d..8e34a42b3 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 2c3dc1857..0343db68b 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -317,7 +317,7 @@ namespace Emby.Server.Implementations.Library } } - var mediaTypes = new List(); + var mediaTypes = new List(); if (includeItemTypes.Length == 0) { diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs index df45793c3..89f64ee4f 100644 --- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators { var movies = _libraryManager.GetItemList(new InternalItemsQuery { - MediaTypes = new string[] { MediaType.Video }, + MediaTypes = new[] { MediaType.Video }, IncludeItemTypes = new[] { BaseItemKind.Movie }, IsVirtualItem = false, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 649c49924..c783f2b01 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -74,7 +75,7 @@ namespace Emby.Server.Implementations.Playlists throw new ArgumentException(nameof(parentFolder)); } - if (string.IsNullOrEmpty(options.MediaType)) + if (options.MediaType is null || options.MediaType == MediaType.Unknown) { foreach (var itemId in options.ItemIdList) { @@ -84,7 +85,7 @@ namespace Emby.Server.Implementations.Playlists throw new ArgumentException("No item exists with the supplied Id"); } - if (!string.IsNullOrEmpty(item.MediaType)) + if (item.MediaType == MediaType.Unknown) { options.MediaType = item.MediaType; } @@ -102,20 +103,20 @@ namespace Emby.Server.Implementations.Playlists { options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist) .Select(i => i.MediaType) - .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + .FirstOrDefault(i => i != MediaType.Unknown); } } - if (!string.IsNullOrEmpty(options.MediaType)) + if (options.MediaType is null || options.MediaType == MediaType.Unknown) { break; } } } - if (string.IsNullOrEmpty(options.MediaType)) + if (options.MediaType is null || options.MediaType == MediaType.Unknown) { - options.MediaType = "Audio"; + options.MediaType = MediaType.Audio; } var user = _userManager.GetUserById(options.UserId); @@ -168,7 +169,7 @@ namespace Emby.Server.Implementations.Playlists return path; } - private List GetPlaylistItems(IEnumerable itemIds, string playlistMediaType, User user, DtoOptions options) + private List GetPlaylistItems(IEnumerable itemIds, MediaType playlistMediaType, User user, DtoOptions options) { var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i is not null); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 6ad6c4cbd..1d72ceae9 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index c9d2f67f9..1c994ac8b 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -95,7 +95,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -299,7 +299,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index d51a5325f..baeb8b81a 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -50,7 +50,7 @@ public class FilterController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.Value.Equals(default) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 80128536d..884613afa 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -195,7 +195,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? isPlayed, @@ -652,7 +652,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? isPlayed, @@ -812,7 +812,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 8d2a738d4..c4c89ccde 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; @@ -75,7 +76,7 @@ public class PlaylistsController : BaseJellyfinApiController [FromQuery, ParameterObsolete] string? name, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList ids, [FromQuery, ParameterObsolete] Guid? userId, - [FromQuery, ParameterObsolete] string? mediaType, + [FromQuery, ParameterObsolete] MediaType? mediaType, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest) { if (ids.Count == 0) diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 387b3ea5a..5b4594165 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -86,7 +86,7 @@ public class SearchController : BaseJellyfinApiController [FromQuery, Required] string searchTerm, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery] Guid? parentId, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e93456de6..e20cf034d 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -393,7 +393,7 @@ public class SessionController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task PostCapabilities( [FromQuery] string? id, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsSync = false, diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 5b808f257..675757fc5 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -56,7 +56,7 @@ public class SuggestionsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSuggestions( [FromRoute, Required] Guid userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, [FromQuery] int? startIndex, [FromQuery] int? limit, diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index b5b640620..9f1e0e837 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -160,7 +160,7 @@ public class TrailersController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? isPlayed, diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 74370db50..200ecea09 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -76,7 +76,7 @@ public class YearsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -191,7 +191,7 @@ public class YearsController : BaseJellyfinApiController return _dtoService.GetBaseItemDto(item, dtoOptions); } - private bool FilterItem(BaseItem f, IReadOnlyCollection excludeItemTypes, IReadOnlyCollection includeItemTypes, IReadOnlyCollection mediaTypes) + private bool FilterItem(BaseItem f, IReadOnlyCollection excludeItemTypes, IReadOnlyCollection includeItemTypes, IReadOnlyCollection mediaTypes) { var baseItemKind = f.GetBaseItemKind(); // Exclude item types @@ -207,7 +207,7 @@ public class YearsController : BaseJellyfinApiController } // Include MediaTypes - if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType)) { return false; } diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index a36028cfe..321987ca7 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -243,7 +243,7 @@ public class MediaInfoHelper } // Beginning of Playback Determination - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + var streamInfo = item.MediaType == MediaType.Audio ? streamBuilder.GetOptimalAudioStream(options) : streamBuilder.GetOptimalVideoStream(options); diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index a653c5795..ed2358dd8 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -128,7 +129,7 @@ public static class StreamingHelpers var item = libraryManager.GetItemById(streamingRequest.Id); - state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); + state.IsInputVideo = item.MediaType == MediaType.Video; MediaSourceInfo? mediaSource = null; if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)) diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index 1fba32c5b..bdc488871 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; namespace Jellyfin.Api.Models.PlaylistDtos; @@ -29,5 +30,5 @@ public class CreatePlaylistDto /// /// Gets or sets the media type. /// - public string? MediaType { get; set; } + public MediaType? MediaType { get; set; } } diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs index b88be33b2..b021771a0 100644 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Session; @@ -16,7 +17,7 @@ public class ClientCapabilitiesDto /// Gets or sets the list of playable media types. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList PlayableMediaTypes { get; set; } = Array.Empty(); + public IReadOnlyList PlayableMediaTypes { get; set; } = Array.Empty(); /// /// Gets or sets the list of supported commands. diff --git a/Jellyfin.Data/Enums/MediaType.cs b/Jellyfin.Data/Enums/MediaType.cs new file mode 100644 index 000000000..b014ff366 --- /dev/null +++ b/Jellyfin.Data/Enums/MediaType.cs @@ -0,0 +1,32 @@ +namespace Jellyfin.Data.Enums; + +/// +/// Media types. +/// +public enum MediaType +{ + /// + /// Unknown media type. + /// + Unknown = 0, + + /// + /// Video media. + /// + Video = 1, + + /// + /// Audio media. + /// + Audio = 2, + + /// + /// Photo media. + /// + Photo = 3, + + /// + /// Book media. + /// + Book = 4 +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs index 27726a57a..8a33383e3 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; @@ -89,14 +90,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session return name; } - private static string GetPlaybackNotificationType(string mediaType) + private static string GetPlaybackNotificationType(MediaType mediaType) { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Audio) { return NotificationType.AudioPlayback.ToString(); } - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Video) { return NotificationType.VideoPlayback.ToString(); } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index 6b16477aa..4c2effc2e 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; @@ -97,14 +98,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session return name; } - private static string? GetPlaybackStoppedNotificationType(string mediaType) + private static string? GetPlaybackStoppedNotificationType(MediaType mediaType) { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Audio) { return NotificationType.AudioPlaybackStopped.ToString(); } - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + if (mediaType == MediaType.Video) { return NotificationType.VideoPlaybackStopped.ToString(); } diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index c7216a320..243d2f04f 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// /// The type of the media. [JsonIgnore] - public override string MediaType => Model.Entities.MediaType.Audio; + public override MediaType MediaType => MediaType.Audio; public override double GetDefaultPrimaryImageAspectRatio() { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 9f3e8eec9..f83065a01 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -422,7 +422,7 @@ namespace MediaBrowser.Controller.Entities /// /// The type of the media. [JsonIgnore] - public virtual string MediaType => null; + public virtual MediaType MediaType => MediaType.Unknown; [JsonIgnore] public virtual string[] PhysicalLocations diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index d75beb06d..63f9180fd 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Entities } [JsonIgnore] - public override string MediaType => Model.Entities.MediaType.Book; + public override MediaType MediaType => MediaType.Book; public override bool SupportsPlayedStatus => true; diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index a51299284..2de974349 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Controller.Entities ImageTypes = Array.Empty(); IncludeItemTypes = Array.Empty(); ItemIds = Array.Empty(); - MediaTypes = Array.Empty(); + MediaTypes = Array.Empty(); MinSimilarityScore = 20; OfficialRatings = Array.Empty(); OrderBy = Array.Empty<(string, SortOrder)>(); @@ -86,7 +86,7 @@ namespace MediaBrowser.Controller.Entities public bool? IncludeItemsByName { get; set; } - public string[] MediaTypes { get; set; } + public MediaType[] MediaTypes { get; set; } public BaseItemKind[] IncludeItemTypes { get; set; } diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index ba6ce189a..cb9feacd3 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; namespace MediaBrowser.Controller.Entities @@ -13,7 +14,7 @@ namespace MediaBrowser.Controller.Entities public override bool SupportsLocalMetadata => false; [JsonIgnore] - public override string MediaType => Model.Entities.MediaType.Photo; + public override MediaType MediaType => MediaType.Photo; [JsonIgnore] public override Folder LatestItemsIndexContainer => AlbumEntity; diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index c276ab463..5f187e64e 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -476,7 +476,7 @@ namespace MediaBrowser.Controller.Entities public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager) { - if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType)) { return false; } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 9f685b7e2..be2eb4d28 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; @@ -256,7 +257,7 @@ namespace MediaBrowser.Controller.Entities /// /// The type of the media. [JsonIgnore] - public override string MediaType => Model.Entities.MediaType.Video; + public override MediaType MediaType => MediaType.Video; public override List GetUserDataKeys() { diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index f11e3c8f6..3c2cf8e3d 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.LiveTv public override LocationType LocationType => LocationType.Remote; [JsonIgnore] - public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; + public override MediaType MediaType => ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video; [JsonIgnore] public bool IsMovie { get; set; } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 498df5ab0..ca032e7f6 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -69,7 +69,7 @@ namespace MediaBrowser.Controller.Playlists public override bool SupportsInheritedParentImages => false; [JsonIgnore] - public override bool SupportsPlayedStatus => string.Equals(MediaType, "Video", StringComparison.OrdinalIgnoreCase); + public override bool SupportsPlayedStatus => MediaType == Jellyfin.Data.Enums.MediaType.Video; [JsonIgnore] public override bool AlwaysScanInternalMetadataPath => true; @@ -80,10 +80,10 @@ namespace MediaBrowser.Controller.Playlists [JsonIgnore] public override bool IsPreSorted => true; - public string PlaylistMediaType { get; set; } + public MediaType PlaylistMediaType { get; set; } [JsonIgnore] - public override string MediaType => PlaylistMediaType; + public override MediaType MediaType => PlaylistMediaType; [JsonIgnore] private bool IsSharedItem @@ -107,9 +107,9 @@ namespace MediaBrowser.Controller.Playlists return System.IO.Path.HasExtension(path) && !Directory.Exists(path); } - public void SetMediaType(string value) + public void SetMediaType(MediaType? value) { - PlaylistMediaType = value; + PlaylistMediaType = value ?? MediaType.Unknown; } public override double GetDefaultPrimaryImageAspectRatio() @@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Playlists return base.GetChildren(user, true, query); } - public static List GetPlaylistItems(string playlistMediaType, IEnumerable inputItems, User user, DtoOptions options) + public static List GetPlaylistItems(MediaType playlistMediaType, IEnumerable inputItems, User user, DtoOptions options) { if (user is not null) { @@ -185,7 +185,7 @@ namespace MediaBrowser.Controller.Playlists return list; } - private static IEnumerable GetPlaylistItems(BaseItem item, User user, string mediaType, DtoOptions options) + private static IEnumerable GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options) { if (item is MusicGenre musicGenre) { diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 25bf23d61..07ad529b4 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; @@ -60,13 +61,13 @@ namespace MediaBrowser.Controller.Session /// Gets the playable media types. /// /// The playable media types. - public IReadOnlyList PlayableMediaTypes + public IReadOnlyList PlayableMediaTypes { get { if (Capabilities is null) { - return Array.Empty(); + return Array.Empty(); } return Capabilities.PlayableMediaTypes; diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index 879a3616b..e0277870d 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Xml; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Playlists; @@ -31,7 +33,11 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "PlaylistMediaType": - item.PlaylistMediaType = reader.ReadNormalizedString(); + if (Enum.TryParse(reader.ReadNormalizedString(), out var mediaType)) + { + item.PlaylistMediaType = mediaType; + } + break; case "PlaylistItems": diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs index 847add07f..3f018cae9 100644 --- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading.Tasks; using System.Xml; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -48,12 +49,12 @@ namespace MediaBrowser.LocalMetadata.Savers { var game = (Playlist)item; - if (string.IsNullOrEmpty(game.PlaylistMediaType)) + if (game.PlaylistMediaType == MediaType.Unknown) { return Task.CompletedTask; } - return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType); + return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType.ToString()); } /// diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 07bb002ea..71d0896a7 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,6 +1,7 @@ #pragma warning disable CA1819 // Properties should not return arrays using System; using System.ComponentModel; +using System.Linq; using System.Xml.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -227,9 +228,12 @@ namespace MediaBrowser.Model.Dlna /// The GetSupportedMediaTypes. /// /// The . - public string[] GetSupportedMediaTypes() + public MediaType[] GetSupportedMediaTypes() { - return ContainerProfile.SplitValue(SupportedMediaTypes); + return ContainerProfile.SplitValue(SupportedMediaTypes) + .Select(m => Enum.TryParse(m, out var parsed) ? parsed : MediaType.Unknown) + .Where(m => m != MediaType.Unknown) + .ToArray(); } /// diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 8fab1ca6d..edbf2d9d4 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -584,7 +584,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the type of the media. /// /// The type of the media. - public string MediaType { get; set; } + public MediaType MediaType { get; set; } /// /// Gets or sets the end date. diff --git a/MediaBrowser.Model/Entities/MediaType.cs b/MediaBrowser.Model/Entities/MediaType.cs deleted file mode 100644 index dd2ae810b..000000000 --- a/MediaBrowser.Model/Entities/MediaType.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MediaBrowser.Model.Entities -{ - /// - /// Class MediaType. - /// - public static class MediaType - { - /// - /// The video. - /// - public const string Video = "Video"; - - /// - /// The audio. - /// - public const string Audio = "Audio"; - - /// - /// The photo. - /// - public const string Photo = "Photo"; - - /// - /// The book. - /// - public const string Book = "Book"; - } -} diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs index 847269716..62d496d04 100644 --- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs +++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Playlists; @@ -22,7 +23,7 @@ public class PlaylistCreationRequest /// /// Gets or sets the media type. /// - public string? MediaType { get; set; } + public MediaType? MediaType { get; set; } /// /// Gets or sets the user id. diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs index 3fa7f3d56..fd911dbed 100644 --- a/MediaBrowser.Model/Search/SearchHint.cs +++ b/MediaBrowser.Model/Search/SearchHint.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Search { Name = string.Empty; MatchedTerm = string.Empty; - MediaType = string.Empty; + MediaType = Jellyfin.Data.Enums.MediaType.Unknown; Artists = Array.Empty(); } @@ -115,7 +115,7 @@ namespace MediaBrowser.Model.Search /// Gets or sets the type of the media. /// /// The type of the media. - public string MediaType { get; set; } + public MediaType MediaType { get; set; } /// /// Gets or sets the start date. diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs index 1caed827f..b91fd8657 100644 --- a/MediaBrowser.Model/Search/SearchQuery.cs +++ b/MediaBrowser.Model/Search/SearchQuery.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Search IncludePeople = true; IncludeStudios = true; - MediaTypes = Array.Empty(); + MediaTypes = Array.Empty(); IncludeItemTypes = Array.Empty(); ExcludeItemTypes = Array.Empty(); } @@ -55,7 +55,7 @@ namespace MediaBrowser.Model.Search public bool IncludeArtists { get; set; } - public string[] MediaTypes { get; set; } + public MediaType[] MediaTypes { get; set; } public BaseItemKind[] IncludeItemTypes { get; set; } diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs index d692906c6..7fefce9cd 100644 --- a/MediaBrowser.Model/Session/ClientCapabilities.cs +++ b/MediaBrowser.Model/Session/ClientCapabilities.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Dlna; namespace MediaBrowser.Model.Session @@ -11,12 +12,12 @@ namespace MediaBrowser.Model.Session { public ClientCapabilities() { - PlayableMediaTypes = Array.Empty(); + PlayableMediaTypes = Array.Empty(); SupportedCommands = Array.Empty(); SupportsPersistentIdentifier = true; } - public IReadOnlyList PlayableMediaTypes { get; set; } + public IReadOnlyList PlayableMediaTypes { get; set; } public IReadOnlyList SupportedCommands { get; set; } diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index f21939d2a..6eb75891a 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -97,7 +97,7 @@ namespace MediaBrowser.Providers.MediaInfo { var query = new InternalItemsQuery { - MediaTypes = new string[] { MediaType.Video }, + MediaTypes = new[] { MediaType.Video }, IsVirtualItem = false, IncludeItemTypes = types, DtoOptions = new DtoOptions(true), -- cgit v1.2.3 From dc27d8f9cd3b6cf280ba8e5e2520db387f651fb4 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Wed, 11 Oct 2023 00:02:37 +0200 Subject: Refactor URI overrides (#10051) --- Emby.Dlna/Main/DlnaEntryPoint.cs | 9 +- Emby.Server.Implementations/ApplicationHost.cs | 6 +- .../EntryPoints/UdpServerEntryPoint.cs | 29 ++- Emby.Server.Implementations/Net/SocketFactory.cs | 33 ++- Emby.Server.Implementations/Udp/UdpServer.cs | 11 +- .../Extensions/NetworkExtensions.cs | 13 +- Jellyfin.Networking/Manager/NetworkManager.cs | 244 +++++++++++++-------- .../Extensions/ApiServiceCollectionExtensions.cs | 2 +- .../Extensions/WebHostBuilderExtensions.cs | 3 +- MediaBrowser.Model/Net/IPData.cs | 5 + .../Net/PublishedServerUriOverride.cs | 42 ++++ RSSDP/SsdpCommunicationsServer.cs | 124 +++++------ RSSDP/SsdpDeviceLocator.cs | 42 ++-- RSSDP/SsdpDevicePublisher.cs | 89 ++++---- .../NetworkExtensionsTests.cs | 1 - .../NetworkManagerTests.cs | 10 +- .../Jellyfin.Networking.Tests/NetworkParseTests.cs | 39 ++-- tests/Jellyfin.Server.Tests/ParseNetworkTests.cs | 5 +- 18 files changed, 431 insertions(+), 276 deletions(-) create mode 100644 MediaBrowser.Model/Net/PublishedServerUriOverride.cs (limited to 'MediaBrowser.Model') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 1a4bec398..83b0ef316 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -201,9 +201,10 @@ namespace Emby.Dlna.Main { if (_communicationsServer is null) { - var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); - - _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) + _communicationsServer = new SsdpCommunicationsServer( + _socketFactory, + _networkManager, + _logger) { IsShared = true }; @@ -213,7 +214,7 @@ namespace Emby.Dlna.Main } catch (Exception ex) { - _logger.LogError(ex, "Error starting ssdp handlers"); + _logger.LogError(ex, "Error starting SSDP handlers"); } } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 3253ea026..c247178ee 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -450,7 +450,7 @@ namespace Emby.Server.Implementations ConfigurationManager.AddParts(GetExports()); - NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger()); + NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger()); // Initialize runtime stat collection if (ConfigurationManager.Configuration.EnableMetrics) @@ -913,7 +913,7 @@ namespace Emby.Server.Implementations /// public string GetSmartApiUrl(HttpRequest request) { - // Return the host in the HTTP request as the API url + // Return the host in the HTTP request as the API URL if not configured otherwise if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) { int? requestPort = request.Host.Port; @@ -948,7 +948,7 @@ namespace Emby.Server.Implementations public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) { // With an empty source, the port will be null - var smart = NetManager.GetBindAddress(ipAddress, out _, true); + var smart = NetManager.GetBindAddress(ipAddress, out _, false); var scheme = !allowHttps ? Uri.UriSchemeHttp : null; int? port = !allowHttps ? HttpPort : null; return GetLocalApiUrl(smart, scheme, port); diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 54191649d..2c03f9ffd 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { /// - /// Class UdpServerEntryPoint. + /// Class responsible for registering all UDP broadcast endpoints and their handlers. /// public sealed class UdpServerEntryPoint : IServerEntryPoint { @@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IConfiguration _config; private readonly IConfigurationManager _configurationManager; private readonly INetworkManager _networkManager; - private readonly bool _enableMultiSocketBinding; /// /// The UDP server. @@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints _configurationManager = configurationManager; _networkManager = networkManager; _udpServers = new List(); - _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); } /// @@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints try { - if (_enableMultiSocketBinding) + // Linux needs to bind to the broadcast addresses to get broadcast traffic + // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses + if (OperatingSystem.IsLinux()) { - // Add global broadcast socket + // Add global broadcast listener var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber); server.Start(_cancellationTokenSource.Token); _udpServers.Add(server); - // Add bind address specific broadcast sockets + // Add bind address specific broadcast listeners // IPv6 is currently unsupported var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); foreach (var intf in validInterfaces) @@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints } else { - var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber); - server.Start(_cancellationTokenSource.Token); - _udpServers.Add(server); + // Add bind address specific broadcast listeners + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); + foreach (var intf in validInterfaces) + { + var intfAddress = intf.Address; + _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber); + + var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber); + server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); + } } } catch (SocketException ex) @@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints { if (_disposed) { - throw new ObjectDisposedException(this.GetType().Name); + throw new ObjectDisposedException(GetType().Name); } } diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 51e92953d..505984cfb 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,12 +1,15 @@ -#pragma warning disable CS1591 - using System; +using System.Linq; using System.Net; +using System.Net.NetworkInformation; using System.Net.Sockets; using MediaBrowser.Model.Net; namespace Emby.Server.Implementations.Net { + /// + /// Factory class to create different kinds of sockets. + /// public class SocketFactory : ISocketFactory { /// @@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net /// public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort) { - ArgumentNullException.ThrowIfNull(bindInterface.Address); + var interfaceAddress = bindInterface.Address; + ArgumentNullException.ThrowIfNull(interfaceAddress); if (localPort < 0) { @@ -49,7 +53,7 @@ namespace Emby.Server.Implementations.Net try { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(new IPEndPoint(bindInterface.Address, localPort)); + socket.Bind(new IPEndPoint(interfaceAddress, localPort)); return socket; } @@ -82,16 +86,25 @@ namespace Emby.Server.Implementations.Net try { - var interfaceIndex = bindInterface.Index; - var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex); - socket.MulticastLoopback = false; socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); - socket.Bind(new IPEndPoint(multicastAddress, localPort)); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress)); + socket.Bind(new IPEndPoint(multicastAddress, localPort)); + } + else + { + // Only create socket if interface supports multicast + var interfaceIndex = bindInterface.Index; + var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex); + + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); + socket.Bind(new IPEndPoint(bindIPAddress, localPort)); + } return socket; } diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index a3bbd6df0..10376ed76 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp _endpoint = new IPEndPoint(bindAddress, port); - _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) + { + MulticastLoopback = false, + }; _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } @@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp try { + _logger.LogDebug("Sending AutoDiscovery response"); await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); } catch (SocketException ex) @@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp { try { - var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false); + var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false); var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes); if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) { @@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp } catch (OperationCanceledException) { - // Don't throw + _logger.LogDebug("Broadcast socket operation cancelled"); } } } diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs index e45fa3bcb..2d921a482 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -231,12 +231,12 @@ public static partial class NetworkExtensions } else if (address.AddressFamily == AddressFamily.InterNetwork) { - result = new IPNetwork(address, Network.MinimumIPv4PrefixSize); + result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize); return true; } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { - result = new IPNetwork(address, Network.MinimumIPv6PrefixSize); + result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize); return true; } } @@ -284,12 +284,15 @@ public static partial class NetworkExtensions if (hosts.Count <= 2) { + var firstPart = hosts[0]; + // Is hostname or hostname:port - if (FqdnGeneratedRegex().IsMatch(hosts[0])) + if (FqdnGeneratedRegex().IsMatch(firstPart)) { try { - addresses = Dns.GetHostAddresses(hosts[0]); + // .NET automatically filters only supported returned addresses based on OS support. + addresses = Dns.GetHostAddresses(firstPart); return true; } catch (SocketException) @@ -299,7 +302,7 @@ public static partial class NetworkExtensions } // Is an IPv4 or IPv4:port - if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) + if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address)) { if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index f20e28526..9c59500d7 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -15,7 +15,9 @@ using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Jellyfin.Networking.Manager { @@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager private readonly IConfigurationManager _configurationManager; + private readonly IConfiguration _startupConfig; + private readonly object _networkEventLock; /// /// Holds the published server URLs and the IPs to use them on. /// - private IReadOnlyDictionary _publishedServerUrls; + private IReadOnlyList _publishedServerUrls; private IReadOnlyList _remoteAddressFilter; @@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager /// /// Initializes a new instance of the class. /// - /// IServerConfigurationManager instance. + /// The instance. + /// The instance holding startup parameters. /// Logger to use for messages. #pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this. - public NetworkManager(IConfigurationManager configurationManager, ILogger logger) + public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger logger) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(configurationManager); _logger = logger; _configurationManager = configurationManager; + _startupConfig = startupConfig; _initLock = new(); _interfaces = new List(); _macAddresses = new List(); - _publishedServerUrls = new Dictionary(); + _publishedServerUrls = new List(); _networkEventLock = new object(); _remoteAddressFilter = new List(); @@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager /// /// Gets the Published server override list. /// - public IReadOnlyDictionary PublishedServerUrls => _publishedServerUrls; + public IReadOnlyList PublishedServerUrls => _publishedServerUrls; /// public void Dispose() @@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager { if (!_eventfire) { - _logger.LogDebug("Network Address Change Event."); // As network events tend to fire one after the other only fire once every second. _eventfire = true; OnNetworkChange(); @@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager } else { - InitialiseInterfaces(); - InitialiseLan(networkConfig); + InitializeInterfaces(); + InitializeLan(networkConfig); EnforceBindSettings(networkConfig); } + PrintNetworkInformation(networkConfig); NetworkChanged?.Invoke(this, EventArgs.Empty); } finally @@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. /// Generate a list of all active mac addresses that aren't loopback addresses. /// - private void InitialiseInterfaces() + private void InitializeInterfaces() { lock (_initLock) { @@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager try { var nics = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); + .Where(i => i.OperationalStatus == OperationalStatus.Up); foreach (NetworkInterface adapter in nics) { @@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager { if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); - interfaceObject.Index = ipProperties.GetIPv4Properties().Index; - interfaceObject.Name = adapter.Name; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) + { + Index = ipProperties.GetIPv4Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; interfaces.Add(interfaceObject); } else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); - interfaceObject.Index = ipProperties.GetIPv6Properties().Index; - interfaceObject.Name = adapter.Name; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) + { + Index = ipProperties.GetIPv6Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; interfaces.Add(interfaceObject); } } } -#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types { // Ignore error, and attempt to continue. _logger.LogError(ex, "Error encountered parsing interfaces."); } } } -#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types { _logger.LogError(ex, "Error obtaining interfaces."); } @@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager { _logger.LogWarning("No interface information available. Using loopback interface(s)."); - if (IsIPv4Enabled && !IsIPv6Enabled) + if (IsIPv4Enabled) { - interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } - if (!IsIPv4Enabled && IsIPv6Enabled) + if (IsIPv6Enabled) { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } } @@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager } /// - /// Initialises internal LAN cache. + /// Initializes internal LAN cache. /// - private void InitialiseLan(NetworkConfiguration config) + private void InitializeLan(NetworkConfiguration config) { lock (_initLock) { @@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true) ? excludedSubnets : new List(); - - _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); } } @@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager .ToHashSet(); interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); - if (bindAddresses.Contains(IPAddress.Loopback)) + if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) { interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } - if (bindAddresses.Contains(IPAddress.IPv6Loopback)) + if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) { interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } @@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); } - _logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); _interfaces = interfaces; } } /// - /// Initialises the remote address values. + /// Initializes the remote address values. /// - private void InitialiseRemote(NetworkConfiguration config) + private void InitializeRemote(NetworkConfiguration config) { lock (_initLock) { @@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager /// format is subnet=ipaddress|host|uri /// when subnet = 0.0.0.0, any external address matches. /// - private void InitialiseOverrides(NetworkConfiguration config) + private void InitializeOverrides(NetworkConfiguration config) { lock (_initLock) { - var publishedServerUrls = new Dictionary(); - var overrides = config.PublishedServerUriBySubnet; + var publishedServerUrls = new List(); + + // Prefer startup configuration. + var startupOverrideKey = _startupConfig[AddressOverrideKey]; + if (!string.IsNullOrEmpty(startupOverrideKey)) + { + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + startupOverrideKey, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + startupOverrideKey, + true, + true)); + _publishedServerUrls = publishedServerUrls; + return; + } + var overrides = config.PublishedServerUriBySubnet; foreach (var entry in overrides) { var parts = entry.Split('='); @@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager var identifier = parts[0]; if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) { - publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; + // Drop any other overrides in case an "all" override exists + publishedServerUrls.Clear(); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + replacement, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + replacement, + true, + true)); + break; } else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) { - publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement; - publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + replacement, + false, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + replacement, + false, + true)); } else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) { foreach (var lan in _lanSubnets) { var lanPrefix = lan.Prefix; - publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), + replacement, + true, + false)); } } else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) { var data = new IPData(result.Prefix, result); - publishedServerUrls[data] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + data, + replacement, + true, + true)); } else if (TryParseInterface(identifier, out var ifaces)) { foreach (var iface in ifaces) { - publishedServerUrls[iface] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + iface, + replacement, + true, + true)); } } else @@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager } /// - /// Reloads all settings and re-initialises the instance. + /// Reloads all settings and re-Initializes the instance. /// /// The to use. public void UpdateSettings(object configuration) @@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager var config = (NetworkConfiguration)configuration; HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6; - InitialiseLan(config); - InitialiseRemote(config); + InitializeLan(config); + InitializeRemote(config); if (string.IsNullOrEmpty(MockNetworkSettings)) { - InitialiseInterfaces(); + InitializeInterfaces(); } else // Used in testing only. { @@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager var index = int.Parse(parts[1], CultureInfo.InvariantCulture); if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { - var data = new IPData(address, subnet, parts[2]); - data.Index = index; + var data = new IPData(address, subnet, parts[2]) + { + Index = index + }; interfaces.Add(data); } } @@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager } EnforceBindSettings(config); - InitialiseOverrides(config); + InitializeOverrides(config); + + PrintNetworkInformation(config, false); } /// @@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager /// public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false) { - if (_interfaces.Count != 0) + if (_interfaces.Count > 0 || individualInterfaces) { return _interfaces; } // No bind address and no exclusions, so listen on all interfaces. var result = new List(); - - if (individualInterfaces) - { - result.AddRange(_interfaces); - return result; - } - if (IsIPv4Enabled && IsIPv6Enabled) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default @@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager bindPreference = string.Empty; int? port = null; - var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any) - || x.Key.Subnet.Contains(source)) - .DistinctBy(x => x.Key) - .OrderBy(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any)) + // Only consider subnets including the source IP, prefering specific overrides + List validPublishedServerUrls; + if (!isInExternalSubnet) + { + // Only use matching internal subnets + // Prefer more specific (bigger subnet prefix) overrides + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source)) + .OrderByDescending(x => x.Data.Subnet.PrefixLength) + .ToList(); + } + else + { + // Only use matching external subnets + // Prefer more specific (bigger subnet prefix) overrides + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source)) + .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); + } - // Check for user override. foreach (var data in validPublishedServerUrls) { - if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any))) - { - // External. - bindPreference = data.Value; - break; - } - - // Get address interface. - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address)); + // Get interface matching override subnet + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address)); if (intf?.Address is not null) { - // Match IP address. - bindPreference = data.Value; + // If matching interface is found, use override + bindPreference = data.OverrideUri; break; } } @@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager return false; } - // Has it got a port defined? + // Handle override specifying port var parts = bindPreference.Split(':'); if (parts.Length > 1) { @@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager { bindPreference = parts[0]; port = p; + _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); + return true; } } - if (port is not null) - { - _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); - } - else - { - _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); - } - + _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); return true; } @@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); return true; } + + private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true) + { + var logLevel = debug ? LogLevel.Debug : LogLevel.Information; + if (_logger.IsEnabled(logLevel)) + { + _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); + _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist"); + _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength)); + } + } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 89dbbdd2f..cb1680558 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } - else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses)) + else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) { foreach (var address in addresses) { diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 3cb791b57..c9d5b54de 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -3,7 +3,6 @@ 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; @@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.NetManager.GetAllBindInterfaces(); + var addresses = appHost.NetManager.GetAllBindInterfaces(true); bool flagged = false; foreach (var netAdd in addresses) diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index 985b16c6e..e9fcd6797 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -47,6 +47,11 @@ public class IPData /// public int Index { get; set; } + /// + /// Gets or sets a value indicating whether the network supports multicast. + /// + public bool SupportsMulticast { get; set; } = false; + /// /// Gets or sets the interface name. /// diff --git a/MediaBrowser.Model/Net/PublishedServerUriOverride.cs b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs new file mode 100644 index 000000000..476d1ba38 --- /dev/null +++ b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs @@ -0,0 +1,42 @@ +namespace MediaBrowser.Model.Net; + +/// +/// Class holding information for a published server URI override. +/// +public class PublishedServerUriOverride +{ + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The override. + /// A value indicating whether the override is for internal requests. + /// A value indicating whether the override is for external requests. + public PublishedServerUriOverride(IPData data, string overrideUri, bool internalOverride, bool externalOverride) + { + Data = data; + OverrideUri = overrideUri; + IsInternalOverride = internalOverride; + IsExternalOverride = externalOverride; + } + + /// + /// Gets or sets the object's IP address. + /// + public IPData Data { get; set; } + + /// + /// Gets or sets the override URI. + /// + public string OverrideUri { get; set; } + + /// + /// Gets or sets a value indicating whether the override should be applied to internal requests. + /// + public bool IsInternalOverride { get; set; } + + /// + /// Gets or sets a value indicating whether the override should be applied to external requests. + /// + public bool IsExternalOverride { get; set; } +} diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 0dce6c3bf..a3f30c174 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -32,10 +32,10 @@ namespace Rssdp.Infrastructure * port to use, we will default to 0 which allows the underlying system to auto-assign a free port. */ - private object _BroadcastListenSocketSynchroniser = new object(); + private object _BroadcastListenSocketSynchroniser = new(); private List _MulticastListenSockets; - private object _SendSocketSynchroniser = new object(); + private object _SendSocketSynchroniser = new(); private List _sendSockets; private HttpRequestParser _RequestParser; @@ -48,7 +48,6 @@ namespace Rssdp.Infrastructure private int _MulticastTtl; private bool _IsShared; - private readonly bool _enableMultiSocketBinding; /// /// Raised when a HTTPU request message is received by a socket (unicast or multicast). @@ -64,9 +63,11 @@ namespace Rssdp.Infrastructure /// Minimum constructor. /// /// The argument is null. - public SsdpCommunicationsServer(ISocketFactory socketFactory, - INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) - : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) + public SsdpCommunicationsServer( + ISocketFactory socketFactory, + INetworkManager networkManager, + ILogger logger) + : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger) { } @@ -76,7 +77,12 @@ namespace Rssdp.Infrastructure /// /// The argument is null. /// The argument is less than or equal to zero. - public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) + public SsdpCommunicationsServer( + ISocketFactory socketFactory, + int localPort, + int multicastTimeToLive, + INetworkManager networkManager, + ILogger logger) { if (socketFactory is null) { @@ -88,19 +94,18 @@ namespace Rssdp.Infrastructure throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero."); } - _BroadcastListenSocketSynchroniser = new object(); - _SendSocketSynchroniser = new object(); + _BroadcastListenSocketSynchroniser = new(); + _SendSocketSynchroniser = new(); _LocalPort = localPort; _SocketFactory = socketFactory; - _RequestParser = new HttpRequestParser(); - _ResponseParser = new HttpResponseParser(); + _RequestParser = new(); + _ResponseParser = new(); _MulticastTtl = multicastTimeToLive; _networkManager = networkManager; _logger = logger; - _enableMultiSocketBinding = enableMultiSocketBinding; } /// @@ -335,7 +340,7 @@ namespace Rssdp.Infrastructure { sockets = sockets.ToList(); - var tasks = sockets.Where(s => (fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))) + var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address)) .Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); return Task.WhenAll(tasks); } @@ -347,33 +352,26 @@ namespace Rssdp.Infrastructure { var sockets = new List(); var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress); - if (_enableMultiSocketBinding) - { - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily == AddressFamily.InterNetwork) - .DistinctBy(x => x.Index); - foreach (var intf in validInterfaces) + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.SupportsMulticast) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork) + .DistinctBy(x => x.Index); + + foreach (var intf in validInterfaces) + { + try { - try - { - var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in CreateMulticastSocketsAndListen. IP address: {0}", intf.Address); - } + var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index); } - } - else - { - var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, new IPData(IPAddress.Any, null), _MulticastTtl, SsdpConstants.MulticastPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); } return sockets; @@ -382,34 +380,32 @@ namespace Rssdp.Infrastructure private List CreateSendSockets() { var sockets = new List(); - if (_enableMultiSocketBinding) + + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.SupportsMulticast) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork); + + if (OperatingSystem.IsMacOS()) { - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily == AddressFamily.InterNetwork); + // Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340 + validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback)); + } - foreach (var intf in validInterfaces) + foreach (var intf in validInterfaces) + { + try { - try - { - var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", intf.Address); - } + var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index); } } - else - { - var socket = _SocketFactory.CreateSsdpUdpSocket(new IPData(IPAddress.Any, null), _LocalPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - return sockets; } @@ -423,7 +419,7 @@ namespace Rssdp.Infrastructure { try { - var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);; + var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);; if (result.ReceivedBytes > 0) { @@ -431,7 +427,7 @@ namespace Rssdp.Infrastructure var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface); ProcessMessage( - UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), + Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), remoteEndpoint, localEndpointAdapter.Address); } @@ -511,7 +507,7 @@ namespace Rssdp.Infrastructure return; } - var handlers = this.RequestReceived; + var handlers = RequestReceived; if (handlers is not null) { handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress)); @@ -520,7 +516,7 @@ namespace Rssdp.Infrastructure private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress) { - var handlers = this.ResponseReceived; + var handlers = ResponseReceived; if (handlers is not null) { handlers(this, new ResponseReceivedEventArgs(data, endPoint) diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 59f4c5070..82b09c4b4 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -17,7 +17,7 @@ namespace Rssdp.Infrastructure private ISsdpCommunicationsServer _CommunicationsServer; private Timer _BroadcastTimer; - private object _timerLock = new object(); + private object _timerLock = new(); private string _OSName; @@ -221,12 +221,12 @@ namespace Rssdp.Infrastructure /// protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress) { - if (this.IsDisposed) + if (IsDisposed) { return; } - var handlers = this.DeviceAvailable; + var handlers = DeviceAvailable; if (handlers is not null) { handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) @@ -244,12 +244,12 @@ namespace Rssdp.Infrastructure /// protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired) { - if (this.IsDisposed) + if (IsDisposed) { return; } - var handlers = this.DeviceUnavailable; + var handlers = DeviceUnavailable; if (handlers is not null) { handlers(this, new DeviceUnavailableEventArgs(device, expired)); @@ -291,8 +291,8 @@ namespace Rssdp.Infrastructure _CommunicationsServer = null; if (commsServer is not null) { - commsServer.ResponseReceived -= this.CommsServer_ResponseReceived; - commsServer.RequestReceived -= this.CommsServer_RequestReceived; + commsServer.ResponseReceived -= CommsServer_ResponseReceived; + commsServer.RequestReceived -= CommsServer_RequestReceived; } } } @@ -341,7 +341,7 @@ namespace Rssdp.Infrastructure var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - values["HOST"] = "239.255.255.250:1900"; + values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort); values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["MAN"] = "\"ssdp:discover\""; @@ -382,17 +382,17 @@ namespace Rssdp.Infrastructure private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress) { - if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) + if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) { return; } var notificationType = GetFirstHeaderStringValue("NTS", message); - if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) + if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) { ProcessAliveNotification(message, IPAddress); } - else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) + else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) { ProcessByeByeNotification(message); } @@ -420,7 +420,7 @@ namespace Rssdp.Infrastructure private void ProcessByeByeNotification(HttpRequestMessage message) { var notficationType = GetFirstHeaderStringValue("NT", message); - if (!String.IsNullOrEmpty(notficationType)) + if (!string.IsNullOrEmpty(notficationType)) { var usn = GetFirstHeaderStringValue("USN", message); @@ -447,10 +447,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message) { string retVal = null; - IEnumerable values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -463,10 +462,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message) { string retVal = null; - IEnumerable values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -479,10 +477,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request) { string value = null; - IEnumerable values; if (request.Headers.Contains(headerName)) { - request.Headers.TryGetValues(headerName, out values); + request.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); @@ -496,10 +493,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response) { string value = null; - IEnumerable values; if (response.Headers.Contains(headerName)) { - response.Headers.TryGetValues(headerName, out values); + response.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); @@ -529,7 +525,7 @@ namespace Rssdp.Infrastructure foreach (var device in expiredDevices) { - if (this.IsDisposed) + if (IsDisposed) { return; } @@ -543,7 +539,7 @@ namespace Rssdp.Infrastructure // problems. foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct()) { - if (this.IsDisposed) + if (IsDisposed) { return; } @@ -560,7 +556,7 @@ namespace Rssdp.Infrastructure existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn); foreach (var existingDevice in existingDevices) { - if (this.IsDisposed) + if (IsDisposed) { return true; } diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 950e6fec8..65ae658a4 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -206,9 +206,9 @@ namespace Rssdp.Infrastructure IPAddress receivedOnlocalIPAddress, CancellationToken cancellationToken) { - if (String.IsNullOrEmpty(searchTarget)) + if (string.IsNullOrEmpty(searchTarget)) { - WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); + WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); return; } @@ -232,7 +232,7 @@ namespace Rssdp.Infrastructure // return; } - if (!Int32.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) + if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) { return; } @@ -243,27 +243,27 @@ namespace Rssdp.Infrastructure } // Do not block synchronously as that may tie up a threadpool thread for several seconds. - Task.Delay(_Random.Next(16, (maxWaitInterval * 1000))).ContinueWith((parentTask) => + Task.Delay(_Random.Next(16, maxWaitInterval * 1000)).ContinueWith((parentTask) => { // Copying devices to local array here to avoid threading issues/enumerator exceptions. IEnumerable devices = null; lock (_Devices) { - if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) + if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) { devices = GetAllDevicesAsFlatEnumerable().ToArray(); } - else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) + else if (string.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) { devices = _Devices.ToArray(); } else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) { - devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) { - devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } } @@ -299,7 +299,7 @@ namespace Rssdp.Infrastructure if (isRootDevice) { SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); } @@ -312,7 +312,7 @@ namespace Rssdp.Infrastructure private string GetUsn(string udn, string fullDeviceType) { - return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); + return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); } private async void SendSearchResponse( @@ -326,16 +326,17 @@ namespace Rssdp.Infrastructure const string header = "HTTP/1.1 200 OK"; var rootDevice = device.ToRootDevice(); - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - values["EXT"] = ""; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["HOST"] = "239.255.255.250:1900"; - values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; - values["ST"] = searchTarget; - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["USN"] = uniqueServiceName; - values["LOCATION"] = rootDevice.Location.ToString(); + var values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["EXT"] = "", + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds, + ["ST"] = searchTarget, + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["USN"] = uniqueServiceName, + ["LOCATION"] = rootDevice.Location.ToString() + }; var message = BuildMessage(header, values); @@ -439,7 +440,7 @@ namespace Rssdp.Infrastructure if (isRoot) { SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken); } @@ -460,17 +461,18 @@ namespace Rssdp.Infrastructure const string header = "NOTIFY * HTTP/1.1"; - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; - values["LOCATION"] = rootDevice.Location.ToString(); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["NTS"] = "ssdp:alive"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; + var values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // If needed later for non-server devices, these headers will need to be dynamic + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds, + ["LOCATION"] = rootDevice.Location.ToString(), + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["NTS"] = "ssdp:alive", + ["NT"] = notificationType, + ["USN"] = uniqueServiceName + }; var message = BuildMessage(header, values); @@ -485,7 +487,7 @@ namespace Rssdp.Infrastructure if (isRoot) { tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken)); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken)); } @@ -506,20 +508,21 @@ namespace Rssdp.Infrastructure { const string header = "NOTIFY * HTTP/1.1"; - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["NTS"] = "ssdp:byebye"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; + var values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // If needed later for non-server devices, these headers will need to be dynamic + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["NTS"] = "ssdp:byebye", + ["NT"] = notificationType, + ["USN"] = uniqueServiceName + }; var message = BuildMessage(header, values); var sendCount = IsDisposed ? 1 : 3; - WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device); + WriteTrace(string.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device); return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); } diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs index 2ff7c7de4..072e0a8c5 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs @@ -16,7 +16,6 @@ namespace Jellyfin.Networking.Tests [InlineData("127.0.0.1:123")] [InlineData("localhost")] [InlineData("localhost:1345")] - [InlineData("www.google.co.uk")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs index 0b07a3c53..2302f90b8 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -1,7 +1,9 @@ using System.Net; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Xunit; namespace Jellyfin.Networking.Tests @@ -28,7 +30,8 @@ namespace Jellyfin.Networking.Tests LocalNetworkSubnets = network.Split(',') }; - using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); Assert.True(networkManager.IsInLocalNetwork(ip)); } @@ -56,9 +59,10 @@ namespace Jellyfin.Networking.Tests LocalNetworkSubnets = network.Split(',') }; - using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); - Assert.False(nm.IsInLocalNetwork(ip)); + Assert.False(networkManager.IsInLocalNetwork(ip)); } } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 731cbbafb..022b8a3d0 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -7,6 +7,7 @@ using Jellyfin.Networking.Extensions; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Net; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -54,7 +55,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]"); @@ -200,7 +202,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11"; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; // Check to see if DNS resolution is working. If not, skip test. @@ -229,24 +232,24 @@ namespace Jellyfin.Networking.Tests [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")] // User on external network, we're bound internal and external - so result is override. - [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "all=http://helloworld.com", "http://helloworld.com")] // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override. - [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] + [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "external=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] // User on internal network, no binding specified - so result is the 1st internal. - [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "external=http://helloworld.com", "eth16")] // User on external network, internal binding only - so assumption is a proxy forward, return external override. - [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=http://helloworld.com", "http://helloworld.com")] // User on external network, no binding - so result is the 1st external which is overriden. - [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=http://helloworld.com", "http://helloworld.com")] - // User assumed to be internal, no binding - so result is the 1st internal. - [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + // User assumed to be internal, no binding - so result is the 1st matching interface. + [InlineData("", "192.168.1.0/24", "", false, "all=http://helloworld.com", "eth16")] - // User is internal, no binding - so result is the 1st internal, which is then overridden. + // User is internal, no binding - so result is the 1st internal interface, which is then overridden. [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")] public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) { @@ -264,7 +267,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11"; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; if (nm.TryParseInterface(result, out IReadOnlyList? resultObj) && resultObj is not null) @@ -293,7 +297,9 @@ namespace Jellyfin.Networking.Tests RemoteIPFilter = addresses.Split(','), IsRemoteIPFilterBlacklist = false }; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } @@ -314,7 +320,8 @@ namespace Jellyfin.Networking.Tests IsRemoteIPFilterBlacklist = true }; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } @@ -334,7 +341,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); var interfaceToUse = nm.GetBindAddress(string.Empty, out _); @@ -358,7 +366,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); var interfaceToUse = nm.GetBindAddress(source, out _); diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 49516cccc..288102037 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -7,6 +7,7 @@ using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -119,8 +120,8 @@ namespace Jellyfin.Server.Tests EnableIPv6 = true, EnableIPv4 = true, }; - - return new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + return new NetworkManager(GetMockConfig(conf), startupConf.Object, new NullLogger()); } } } -- cgit v1.2.3 From 26571a8c51b9670f198e58175463e1d3db5441ee Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 11 Oct 2023 01:51:15 +0200 Subject: Deprecate CanLaunchWebBrowser (#10381) It's been a while since I removed this feature from server not sure why it's in the api anyway. The macOS and Windows app have this functionality --- Emby.Server.Implementations/SystemManager.cs | 5 ----- MediaBrowser.Model/System/SystemInfo.cs | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index 5e9c424e9..af66b62e3 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -46,10 +46,6 @@ public class SystemManager : ISystemManager _installationManager = installationManager; } - private bool CanLaunchWebBrowser => Environment.UserInteractive - && !_startupOptions.IsService - && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); - /// public SystemInfo GetSystemInfo(HttpRequest request) { @@ -67,7 +63,6 @@ public class SystemManager : ISystemManager ItemsByNamePath = _applicationPaths.InternalMetadataPath, InternalMetadataPath = _applicationPaths.InternalMetadataPath, CachePath = _applicationPaths.CachePath, - CanLaunchWebBrowser = CanLaunchWebBrowser, TranscodingTempPath = _configurationManager.GetTranscodePath(), ServerName = _applicationHost.FriendlyName, LocalAddress = _applicationHost.GetSmartApiUrl(request), diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index bd0099af7..502fc38ab 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -84,7 +84,8 @@ namespace MediaBrowser.Model.System [Obsolete("This is always true")] public bool CanSelfRestart { get; set; } = true; - public bool CanLaunchWebBrowser { get; set; } + [Obsolete("This is always false")] + public bool CanLaunchWebBrowser { get; set; } = false; /// /// Gets or sets the program data path. -- cgit v1.2.3 From 9f259aa404a4bf4185f22ece9a13be2a7ae346df Mon Sep 17 00:00:00 2001 From: chinkara Date: Sun, 15 Oct 2023 15:53:53 +0200 Subject: add EnableSubtitleManagement permission --- Jellyfin.Data/Entities/User.cs | 1 + Jellyfin.Data/Enums/PermissionKind.cs | 7 ++++++- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 ++ MediaBrowser.Model/Users/UserPolicy.cs | 8 ++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) (limited to 'MediaBrowser.Model') diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 58ddaaf83..8e66a3287 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -499,6 +499,7 @@ namespace Jellyfin.Data.Entities Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false)); Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false)); + Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false)); } /// diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs index 40280b95e..6644f0151 100644 --- a/Jellyfin.Data/Enums/PermissionKind.cs +++ b/Jellyfin.Data/Enums/PermissionKind.cs @@ -113,6 +113,11 @@ namespace Jellyfin.Data.Enums /// /// Whether the user can create, modify and delete collections. /// - EnableCollectionManagement = 21 + EnableCollectionManagement = 21, + + /// + /// Whether the user can edit subtitles. + /// + EnableSubtitleManagement = 22 } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 94ac4798c..59d655c82 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -349,6 +349,7 @@ namespace Jellyfin.Server.Implementations.Users ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement), + EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement), AccessSchedules = user.AccessSchedules.ToArray(), BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), AllowedTags = user.GetPreference(PreferenceKind.AllowedTags), @@ -666,6 +667,7 @@ namespace Jellyfin.Server.Implementations.Users user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement); + user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 8354c60ef..f5aff07db 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -15,6 +15,7 @@ namespace MediaBrowser.Model.Users { IsHidden = true; EnableCollectionManagement = false; + EnableSubtitleManagement = false; EnableContentDeletion = false; EnableContentDeletionFromFolders = Array.Empty(); @@ -83,6 +84,13 @@ namespace MediaBrowser.Model.Users [DefaultValue(false)] public bool EnableCollectionManagement { get; set; } + /// + /// Gets or sets a value indicating whether this instance can manage subtitles. + /// + /// true if this instance is allowed; otherwise, false. + [DefaultValue(false)] + public bool EnableSubtitleManagement { get; set; } + /// /// Gets or sets a value indicating whether this instance is disabled. /// -- cgit v1.2.3 From 6c5a4a93ccdb7c6ca8b26c36505d0954a7c79300 Mon Sep 17 00:00:00 2001 From: herby2212 <12448284+herby2212@users.noreply.github.com> Date: Sat, 21 Oct 2023 11:49:01 +0200 Subject: fix indentation after merge --- .../Configuration/ServerConfiguration.cs | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'MediaBrowser.Model') diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 2fb5fbd0b..fe92251e9 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -158,20 +158,20 @@ public class ServerConfiguration : BaseApplicationConfiguration /// The remaining time in minutes. public int MaxAudiobookResume { get; set; } = 5; - /// - /// Gets or sets the threshold in minutes after a inactive session gets closed automatically. - /// If set to 0 the check for inactive sessions gets disabled. - /// - /// The close inactive session threshold in minutes. 0 to disable. - public int InactiveSessionThreshold { get; set; } = 10; - - /// - /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed - /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several - /// different directories and files. - /// - /// The file watcher delay. - public int LibraryMonitorDelay { get; set; } = 60; + /// + /// Gets or sets the threshold in minutes after a inactive session gets closed automatically. + /// If set to 0 the check for inactive sessions gets disabled. + /// + /// The close inactive session threshold in minutes. 0 to disable. + public int InactiveSessionThreshold { get; set; } = 10; + + /// + /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed + /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several + /// different directories and files. + /// + /// The file watcher delay. + public int LibraryMonitorDelay { get; set; } = 60; /// /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification. -- cgit v1.2.3 From a9ef103c95a7460031879726f4afda3013ca6619 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 5 Nov 2023 02:01:14 +0100 Subject: Add IDisposableAnalyzers to more projects --- Emby.Dlna/Emby.Dlna.csproj | 6 +++++- Emby.Dlna/PlayTo/Device.cs | 5 ++--- Emby.Dlna/PlayTo/PlayToController.cs | 13 ++++++------- Emby.Naming/Emby.Naming.csproj | 6 +++++- Emby.Photos/Emby.Photos.csproj | 8 ++++++-- .../Emby.Server.Implementations.csproj | 7 ++++++- Jellyfin.Api/Jellyfin.Api.csproj | 6 +++++- .../ActivityLogWebSocketListener.cs | 5 ++++- .../ScheduledTasksWebSocketListener.cs | 7 +++++-- .../SessionInfoWebSocketListener.cs | 17 ++++++++++------- Jellyfin.Data/Jellyfin.Data.csproj | 6 +++++- Jellyfin.Networking/Jellyfin.Networking.csproj | 6 +++++- .../Jellyfin.Server.Implementations.csproj | 2 +- Jellyfin.Server/Jellyfin.Server.csproj | 6 +++++- MediaBrowser.Common/MediaBrowser.Common.csproj | 6 +++++- .../MediaBrowser.Controller.csproj | 6 +++++- .../MediaBrowser.LocalMetadata.csproj | 6 +++++- .../MediaBrowser.MediaEncoding.csproj | 6 +++++- .../Subtitles/SubtitleEncoder.cs | 8 +++----- MediaBrowser.Model/MediaBrowser.Model.csproj | 7 ++++++- MediaBrowser.Providers/Manager/ProviderManager.cs | 10 +++++----- .../MediaBrowser.Providers.csproj | 6 +++++- .../MediaBrowser.XbmcMetadata.csproj | 4 ++++ src/Directory.Build.props | 21 +++++++++++++++++++++ .../Jellyfin.Drawing.Skia.csproj | 15 --------------- src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 15 --------------- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 16 ---------------- .../Jellyfin.MediaEncoding.Hls.csproj | 15 --------------- .../Jellyfin.MediaEncoding.Keyframes.csproj | 15 --------------- 29 files changed, 134 insertions(+), 122 deletions(-) create mode 100644 src/Directory.Build.props (limited to 'MediaBrowser.Model') diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index aca239964..efbef0564 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -26,8 +26,12 @@ false - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index bb9b8b0fd..18fa19650 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1248,11 +1248,10 @@ namespace Emby.Dlna.PlayTo if (disposing) { _timer?.Dispose(); + _timer = null; + Properties = null!; } - _timer = null; - Properties = null!; - _disposed = true; } diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index b1ad15cdc..df02fe631 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -683,16 +683,15 @@ namespace Emby.Dlna.PlayTo if (disposing) { + _device.PlaybackStart -= OnDevicePlaybackStart; + _device.PlaybackProgress -= OnDevicePlaybackProgress; + _device.PlaybackStopped -= OnDevicePlaybackStopped; + _device.MediaChanged -= OnDeviceMediaChanged; + _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; + _device.OnDeviceUnavailable = null; _device.Dispose(); } - _device.PlaybackStart -= OnDevicePlaybackStart; - _device.PlaybackProgress -= OnDevicePlaybackProgress; - _device.PlaybackStopped -= OnDevicePlaybackStopped; - _device.MediaChanged -= OnDeviceMediaChanged; - _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; - _device.OnDeviceUnavailable = null; - _disposed = true; } diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index f3973dad9..bc7548189 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -45,8 +45,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 0f97a0686..5a04bbe49 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -24,14 +24,18 @@ true - + + + all + runtime; build; native; contentfiles; analyzers + 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 80263c139..b48e389ac 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -49,8 +49,13 @@ false - + + + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 7ac231885..03dd97367 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -24,8 +24,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 5b90d65d8..ba228cb00 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -53,7 +53,10 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener protected override void Dispose(bool dispose) { - _activityManager.EntryCreated -= OnEntryCreated; + if (dispose) + { + _activityManager.EntryCreated -= OnEntryCreated; + } base.Dispose(dispose); } diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs index a9df2d671..37c108d5a 100644 --- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs @@ -58,8 +58,11 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener protected override void Dispose(bool dispose) { - _taskManager.TaskExecuting -= OnTaskExecuting; - _taskManager.TaskCompleted -= OnTaskCompleted; + if (dispose) + { + _taskManager.TaskExecuting -= OnTaskExecuting; + _taskManager.TaskCompleted -= OnTaskCompleted; + } base.Dispose(dispose); } diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index b403ff46d..3c2b86142 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -57,13 +57,16 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener protected override void Dispose(bool dispose) { - _sessionManager.SessionStarted -= OnSessionManagerSessionStarted; - _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; - _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart; - _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; - _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress; - _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged; - _sessionManager.SessionActivity -= OnSessionManagerSessionActivity; + if (dispose) + { + _sessionManager.SessionStarted -= OnSessionManagerSessionStarted; + _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; + _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart; + _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; + _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress; + _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged; + _sessionManager.SessionActivity -= OnSessionManagerSessionActivity; + } base.Dispose(dispose); } diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 1bc5d8bf9..847853ca0 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -27,8 +27,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 4cff5927f..43d08c37a 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -9,8 +9,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + 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 fa6adb022..df1d5a3e1 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -6,7 +6,7 @@ true - + all diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 62abb8935..5479d2296 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -22,8 +22,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 7015d991f..7d0d7a173 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -43,8 +43,12 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 69c0d26b6..f9468f6cd 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -49,8 +49,12 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 71cdea529..a39bc238a 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -20,8 +20,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index a0624fe76..1f39e88cd 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -29,8 +29,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 21fa4468e..8eea773d8 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -135,19 +135,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles var subtitleStream = mediaSource.MediaStreams .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex); - var subtitle = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken) + var (stream, inputFormat) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken) .ConfigureAwait(false); - var inputFormat = subtitle.Format; - // Return the original if the same format is being requested // Character encoding was already handled in GetSubtitleStream if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase)) { - return subtitle.Stream; + return stream; } - using (var stream = subtitle.Stream) + using (stream) { return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken); } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 58ba83a35..75c5bc6f0 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -48,8 +48,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers @@ -58,6 +62,7 @@ + diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index d0bb34d52..4ba884418 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1096,13 +1096,13 @@ namespace MediaBrowser.Providers.Manager return; } - if (!_disposeCancellationTokenSource.IsCancellationRequested) - { - _disposeCancellationTokenSource.Cancel(); - } - if (disposing) { + if (!_disposeCancellationTokenSource.IsCancellationRequested) + { + _disposeCancellationTokenSource.Cancel(); + } + _disposeCancellationTokenSource.Dispose(); } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 7ef70f4b0..8471f6fa1 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -33,8 +33,12 @@ ../jellyfin.ruleset - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 807234915..d7e34fd22 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -22,6 +22,10 @@ + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 000000000..ac2726ed5 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,21 @@ + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + 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 09e103c6b..3c417f8ff 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -31,19 +31,4 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index 2a5e24a44..d7ef6f8e7 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -21,19 +21,4 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 36ae55ed2..997df6dbe 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -27,24 +27,8 @@ - - - - - all - runtime; build; native; contentfiles; analyzers - - - 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 b792e7ec6..76dde1cf6 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -5,21 +5,6 @@ true - - - - all - runtime; build; native; contentfiles; analyzers - - - 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 09b1f8faa..0d91a447b 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -9,21 +9,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - -- cgit v1.2.3 From c7a94d48ae019f41d5f06340bca7efe0788ad5ad Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Thu, 9 Nov 2023 14:00:13 -0700 Subject: Convert ItemSortBy to enum (#9765) * Convert ItemSortBy to enum * Rename Unknown to Default --- Emby.Dlna/ContentDirectory/ControlHandler.cs | 18 +- .../Data/SqliteItemRepository.cs | 205 +++++---------------- .../Images/BaseFolderImageProvider.cs | 2 +- .../Library/LibraryManager.cs | 8 +- .../LiveTv/LiveTvManager.cs | 2 +- .../Sorting/AiredEpisodeOrderComparer.cs | 3 +- .../Sorting/AlbumArtistComparer.cs | 3 +- .../Sorting/AlbumComparer.cs | 3 +- .../Sorting/ArtistComparer.cs | 3 +- .../Sorting/CommunityRatingComparer.cs | 3 +- .../Sorting/CriticRatingComparer.cs | 3 +- .../Sorting/DateCreatedComparer.cs | 3 +- .../Sorting/DateLastMediaAddedComparer.cs | 3 +- .../Sorting/DatePlayedComparer.cs | 3 +- .../Sorting/IndexNumberComparer.cs | 3 +- .../Sorting/IsFavoriteOrLikeComparer.cs | 3 +- .../Sorting/IsFolderComparer.cs | 3 +- .../Sorting/IsPlayedComparer.cs | 3 +- .../Sorting/IsUnplayedComparer.cs | 3 +- .../Sorting/NameComparer.cs | 3 +- .../Sorting/OfficialRatingComparer.cs | 3 +- .../Sorting/ParentIndexNumberComparer.cs | 3 +- .../Sorting/PlayCountComparer.cs | 3 +- .../Sorting/PremiereDateComparer.cs | 3 +- .../Sorting/ProductionYearComparer.cs | 3 +- .../Sorting/RandomComparer.cs | 3 +- .../Sorting/RuntimeComparer.cs | 3 +- .../Sorting/SeriesSortNameComparer.cs | 3 +- .../Sorting/SortNameComparer.cs | 3 +- .../Sorting/StartDateComparer.cs | 3 +- .../Sorting/StudioComparer.cs | 3 +- Jellyfin.Api/Controllers/ArtistsController.cs | 4 +- Jellyfin.Api/Controllers/ChannelsController.cs | 2 +- Jellyfin.Api/Controllers/GenresController.cs | 2 +- Jellyfin.Api/Controllers/ItemsController.cs | 4 +- Jellyfin.Api/Controllers/LiveTvController.cs | 4 +- Jellyfin.Api/Controllers/MusicGenresController.cs | 2 +- Jellyfin.Api/Controllers/TrailersController.cs | 2 +- Jellyfin.Api/Controllers/TvShowsController.cs | 4 +- Jellyfin.Api/Controllers/YearsController.cs | 2 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 6 +- Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs | 2 +- Jellyfin.Data/Enums/ItemSortBy.cs | 167 +++++++++++++++++ .../Entities/IHasDisplayOrder.cs | 2 + .../Entities/InternalItemsQuery.cs | 4 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 8 +- MediaBrowser.Controller/Library/ILibraryManager.cs | 4 +- MediaBrowser.Controller/Providers/EpisodeInfo.cs | 1 + .../Sorting/IBaseItemComparer.cs | 6 +- MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs | 1 + MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs | 4 +- MediaBrowser.Model/Querying/ItemSortBy.cs | 163 ---------------- MediaBrowser.Providers/Manager/MetadataService.cs | 1 + .../Plugins/Tmdb/TmdbClientManager.cs | 1 + .../Helpers/RequestHelpersTests.cs | 36 ++-- 55 files changed, 329 insertions(+), 416 deletions(-) create mode 100644 Jellyfin.Data/Enums/ItemSortBy.cs delete mode 100644 MediaBrowser.Model/Querying/ItemSortBy.cs (limited to 'MediaBrowser.Model') diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index abd594a3a..e685d252e 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -917,7 +917,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult GetGenres(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; var genresResult = _libraryManager.GetGenres(query); @@ -933,7 +933,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult GetMusicGenres(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; var genresResult = _libraryManager.GetMusicGenres(query); @@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; var artists = _libraryManager.GetAlbumArtists(query); @@ -965,7 +965,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult GetMusicArtists(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; var artists = _libraryManager.GetArtists(query); return ToResult(query.StartIndex, artists); @@ -980,7 +980,7 @@ namespace Emby.Dlna.ContentDirectory private QueryResult GetFavoriteArtists(BaseItem parent, InternalItemsQuery query) { // Don't sort - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); query.AncestorIds = new[] { parent.Id }; query.IsFavorite = true; var artists = _libraryManager.GetArtists(query); @@ -1011,7 +1011,7 @@ namespace Emby.Dlna.ContentDirectory /// The . private QueryResult GetNextUp(BaseItem parent, InternalItemsQuery query) { - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); var result = _tvSeriesManager.GetNextUp( new NextUpQuery @@ -1036,7 +1036,7 @@ namespace Emby.Dlna.ContentDirectory /// The . private QueryResult GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType) { - query.OrderBy = Array.Empty<(string, SortOrder)>(); + query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); var items = _userViewManager.GetLatestItems( new LatestItemsQuery @@ -1203,9 +1203,9 @@ namespace Emby.Dlna.ContentDirectory /// /// The . /// True if pre-sorted. - private static (string SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted) + private static (ItemSortBy SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted) { - return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) }; + return isPreSorted ? Array.Empty<(ItemSortBy, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) }; } /// diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index e519364c2..fadd4f2f3 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2042,7 +2042,7 @@ namespace Emby.Server.Implementations.Data return false; } - var sortingFields = new HashSet(query.OrderBy.Select(i => i.OrderBy), StringComparer.OrdinalIgnoreCase); + var sortingFields = new HashSet(query.OrderBy.Select(i => i.OrderBy)); return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked) || sortingFields.Contains(ItemSortBy.IsPlayed) @@ -2832,20 +2832,20 @@ namespace Emby.Server.Implementations.Data if (hasSimilar || hasSearch) { - List<(string, SortOrder)> prepend = new List<(string, SortOrder)>(4); + List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); if (hasSearch) { - prepend.Add(("SearchScore", SortOrder.Descending)); + prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending)); prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); } if (hasSimilar) { - prepend.Add(("SimilarityScore", SortOrder.Descending)); + prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending)); prepend.Add((ItemSortBy.Random, SortOrder.Ascending)); } - var arr = new (string, SortOrder)[prepend.Count + orderBy.Count]; + var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count]; prepend.CopyTo(arr, 0); orderBy.CopyTo(arr, prepend.Count); orderBy = query.OrderBy = arr; @@ -2863,166 +2863,43 @@ namespace Emby.Server.Implementations.Data })); } - private string MapOrderByField(string name, InternalItemsQuery query) + private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) { - if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase)) - { - // TODO - return "SortName"; - } - - if (string.Equals(name, ItemSortBy.Runtime, StringComparison.OrdinalIgnoreCase)) - { - return "RuntimeTicks"; - } - - if (string.Equals(name, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) - { - return "RANDOM()"; - } - - if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase)) - { - if (query.GroupBySeriesPresentationUniqueKey) - { - return "MAX(LastPlayedDate)"; - } - - return "LastPlayedDate"; - } - - if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.PlayCount; - } - - if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase)) - { - return "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )"; - } - - if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.IsFolder; - } - - if (string.Equals(name, ItemSortBy.IsPlayed, StringComparison.OrdinalIgnoreCase)) - { - return "played"; - } - - if (string.Equals(name, ItemSortBy.IsUnplayed, StringComparison.OrdinalIgnoreCase)) - { - return "played"; - } - - if (string.Equals(name, ItemSortBy.DateLastContentAdded, StringComparison.OrdinalIgnoreCase)) - { - return "DateLastMediaAdded"; - } - - if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase)) - { - return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)"; - } - - if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase)) - { - return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)"; - } - - if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase)) - { - return "InheritedParentalRatingValue"; - } - - if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase)) - { - return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)"; - } - - if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase)) - { - return "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)"; - } - - if (string.Equals(name, ItemSortBy.SeriesSortName, StringComparison.OrdinalIgnoreCase)) - { - return "SeriesName"; - } - - if (string.Equals(name, ItemSortBy.AiredEpisodeOrder, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.AiredEpisodeOrder; - } - - if (string.Equals(name, ItemSortBy.Album, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.Album; - } - - if (string.Equals(name, ItemSortBy.DateCreated, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.DateCreated; - } - - if (string.Equals(name, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.PremiereDate; - } - - if (string.Equals(name, ItemSortBy.StartDate, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.StartDate; - } - - if (string.Equals(name, ItemSortBy.Name, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.Name; - } - - if (string.Equals(name, ItemSortBy.CommunityRating, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.CommunityRating; - } - - if (string.Equals(name, ItemSortBy.ProductionYear, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.ProductionYear; - } - - if (string.Equals(name, ItemSortBy.CriticRating, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.CriticRating; - } - - if (string.Equals(name, ItemSortBy.VideoBitRate, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.VideoBitRate; - } - - if (string.Equals(name, ItemSortBy.ParentIndexNumber, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.ParentIndexNumber; - } - - if (string.Equals(name, ItemSortBy.IndexNumber, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.IndexNumber; - } - - if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.SimilarityScore; - } - - if (string.Equals(name, ItemSortBy.SearchScore, StringComparison.OrdinalIgnoreCase)) - { - return ItemSortBy.SearchScore; - } - - // Unknown SortBy, just sort by the SortName. - return ItemSortBy.SortName; + return sortBy switch + { + ItemSortBy.AirTime => "SortName", // TODO + ItemSortBy.Runtime => "RuntimeTicks", + ItemSortBy.Random => "RANDOM()", + ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)", + ItemSortBy.DatePlayed => "LastPlayedDate", + ItemSortBy.PlayCount => "PlayCount", + ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )", + ItemSortBy.IsFolder => "IsFolder", + ItemSortBy.IsPlayed => "played", + ItemSortBy.IsUnplayed => "played", + ItemSortBy.DateLastContentAdded => "DateLastMediaAdded", + ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)", + ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)", + ItemSortBy.OfficialRating => "InheritedParentalRatingValue", + ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)", + ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => "SeriesName", + ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => "Album", + ItemSortBy.DateCreated => "DateCreated", + ItemSortBy.PremiereDate => "PremiereDate", + ItemSortBy.StartDate => "StartDate", + ItemSortBy.Name => "Name", + ItemSortBy.CommunityRating => "CommunityRating", + ItemSortBy.ProductionYear => "ProductionYear", + ItemSortBy.CriticRating => "CriticRating", + ItemSortBy.VideoBitRate => "VideoBitRate", + ItemSortBy.ParentIndexNumber => "ParentIndexNumber", + ItemSortBy.IndexNumber => "IndexNumber", + ItemSortBy.SimilarityScore => "SimilarityScore", + ItemSortBy.SearchScore => "SearchScore", + _ => "SortName" + }; } public List GetItemIdsList(InternalItemsQuery query) diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index 539d4a63a..04d90af3c 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Images Recursive = true, DtoOptions = new DtoOptions(true), ImageTypes = new ImageType[] { ImageType.Primary }, - OrderBy = new (string, SortOrder)[] + OrderBy = new (ItemSortBy, SortOrder)[] { (ItemSortBy.IsFolder, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 4f0983564..5c76e77be 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1678,7 +1678,7 @@ namespace Emby.Server.Implementations.Library /// The sort by. /// The sort order. /// IEnumerable{BaseItem}. - public IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder) + public IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder) { var isFirst = true; @@ -1701,7 +1701,7 @@ namespace Emby.Server.Implementations.Library return orderedItems ?? items; } - public IEnumerable Sort(IEnumerable items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy) + public IEnumerable Sort(IEnumerable items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy) { var isFirst = true; @@ -1736,9 +1736,9 @@ namespace Emby.Server.Implementations.Library /// The name. /// The user. /// IBaseItemComparer. - private IBaseItemComparer GetComparer(string name, User user) + private IBaseItemComparer GetComparer(ItemSortBy name, User user) { - var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)); + var comparer = Comparers.FirstOrDefault(c => name == c.Type); // If it requires a user, create a new one, and assign the user if (comparer is IUserBaseItemComparer) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index ee039ff0f..dd427c736 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -207,7 +207,7 @@ namespace Emby.Server.Implementations.LiveTv orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending)); } - if (!internalQuery.OrderBy.Any(i => string.Equals(i.OrderBy, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase))) + if (internalQuery.OrderBy.All(i => i.OrderBy != ItemSortBy.SortName)) { orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending)); } diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs index 964004ecc..6d13c6d57 100644 --- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Sorting; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.AiredEpisodeOrder; + public ItemSortBy Type => ItemSortBy.AiredEpisodeOrder; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs index 67a9fbd3b..65c8599e7 100644 --- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Sorting; @@ -16,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.AlbumArtist; + public ItemSortBy Type => ItemSortBy.AlbumArtist; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs index 4bed0fca1..e07113655 100644 --- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Sorting; @@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.Album; + public ItemSortBy Type => ItemSortBy.Album; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs index a8bb55e2b..f99977e5c 100644 --- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Sorting; @@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting public class ArtistComparer : IBaseItemComparer { /// - public string Name => ItemSortBy.Artist; + public ItemSortBy Type => ItemSortBy.Artist; /// public int Compare(BaseItem? x, BaseItem? y) diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs index 5cb11ab46..9e02ea2ae 100644 --- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.CommunityRating; + public ItemSortBy Type => ItemSortBy.CommunityRating; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs index ba1835e4f..d4a8d4689 100644 --- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs @@ -1,3 +1,4 @@ +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.CriticRating; + public ItemSortBy Type => ItemSortBy.CriticRating; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs index 6133aaccc..b86b4432f 100644 --- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.DateCreated; + public ItemSortBy Type => ItemSortBy.DateCreated; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs index b1cb123ce..e1c26d012 100644 --- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs @@ -3,6 +3,7 @@ using System; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -34,7 +35,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.DateLastContentAdded; + public ItemSortBy Type => ItemSortBy.DateLastContentAdded; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs index 453d817c7..d668c17bf 100644 --- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -2,6 +2,7 @@ using System; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -36,7 +37,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.DatePlayed; + public ItemSortBy Type => ItemSortBy.DatePlayed; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs index 1bcaccd8a..11cad6256 100644 --- a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs +++ b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.IndexNumber; + public ItemSortBy Type => ItemSortBy.IndexNumber; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 73e628cf7..622a341b6 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.IsFavoriteOrLiked; + public ItemSortBy Type => ItemSortBy.IsFavoriteOrLiked; /// /// Gets or sets the user data repository. diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs index 3c5ddeefa..6f0ca59c5 100644 --- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.IsFolder; + public ItemSortBy Type => ItemSortBy.IsFolder; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index 7d77a8bc5..2a3e456c2 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.IsUnplayed; + public ItemSortBy Type => ItemSortBy.IsUnplayed; /// /// Gets or sets the user data repository. diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index 926835f90..afd8ccf9f 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.IsUnplayed; + public ItemSortBy Type => ItemSortBy.IsUnplayed; /// /// Gets or sets the user data repository. diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs index 93bec4db9..72d9c7973 100644 --- a/Emby.Server.Implementations/Sorting/NameComparer.cs +++ b/Emby.Server.Implementations/Sorting/NameComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.Name; + public ItemSortBy Type => ItemSortBy.Name; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs index ce44f99a6..b4ee2c723 100644 --- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Globalization; @@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.OfficialRating; + public ItemSortBy Type => ItemSortBy.OfficialRating; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs index c54750843..5aeac29be 100644 --- a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs +++ b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.ParentIndexNumber; + public ItemSortBy Type => ItemSortBy.ParentIndexNumber; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs index 16f1b79b3..12f88bf4d 100644 --- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs +++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs @@ -1,6 +1,7 @@ #nullable disable using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -23,7 +24,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.PlayCount; + public ItemSortBy Type => ItemSortBy.PlayCount; /// /// Gets or sets the user data repository. diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs index db86b8002..8c8b8824f 100644 --- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.PremiereDate; + public ItemSortBy Type => ItemSortBy.PremiereDate; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs index 7fd1e024d..9aec87f18 100644 --- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs +++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs @@ -1,3 +1,4 @@ +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.ProductionYear; + public ItemSortBy Type => ItemSortBy.ProductionYear; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs index bf0168222..6f8ea5b74 100644 --- a/Emby.Server.Implementations/Sorting/RandomComparer.cs +++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.Random; + public ItemSortBy Type => ItemSortBy.Random; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs index 753e58324..3c096ab02 100644 --- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs +++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.Runtime; + public ItemSortBy Type => ItemSortBy.Runtime; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 5b6c64f63..ed42fd6d5 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.SeriesSortName; + public ItemSortBy Type => ItemSortBy.SeriesSortName; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index 19abafe19..314c25d12 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -1,4 +1,5 @@ using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.SortName; + public ItemSortBy Type => ItemSortBy.SortName; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs index 2759d20de..e0b438ef1 100644 --- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Sorting; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.StartDate; + public ItemSortBy Type => ItemSortBy.StartDate; /// /// Compares the specified x. diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs index 89d10f3d2..0edffb783 100644 --- a/Emby.Server.Implementations/Sorting/StudioComparer.cs +++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// Gets the name. /// /// The name. - public string Name => ItemSortBy.Studio; + public ItemSortBy Type => ItemSortBy.Studio; /// /// Compares the specified x. diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index c9d2f67f9..c3c43982b 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -113,7 +113,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) @@ -317,7 +317,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 11c4ac376..fdc16ee23 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -122,7 +122,7 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { userId = RequestHelpers.GetUserId(User, userId); diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index da60f2c60..51f04fa27 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -85,7 +85,7 @@ public class GenresController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 80128536d..891cf88a7 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -197,7 +197,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, @@ -654,7 +654,7 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 649397d68..58159406a 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -143,7 +143,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] SortOrder? sortOrder, [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) @@ -547,7 +547,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? isSports, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 435457af6..94c899357 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -85,7 +85,7 @@ public class MusicGenresController : BaseJellyfinApiController [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index b5b640620..1246efd49 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -162,7 +162,7 @@ public class TrailersController : BaseJellyfinApiController [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index af403fa80..55a30d469 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -219,7 +219,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, - [FromQuery] string? sortBy) + [FromQuery] ItemSortBy? sortBy) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.Value.Equals(default) @@ -289,7 +289,7 @@ public class TvShowsController : BaseJellyfinApiController episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList(); } - if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) + if (sortBy == ItemSortBy.Random) { episodes.Shuffle(); } diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 74370db50..641cdd81e 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -77,7 +77,7 @@ public class YearsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index bc12ca388..be3d4dfb6 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -30,14 +30,14 @@ public static class RequestHelpers /// Sort By. Comma delimited string. /// Sort Order. Comma delimited string. /// Order By. - public static (string, SortOrder)[] GetOrderBy(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder) + public static (ItemSortBy, SortOrder)[] GetOrderBy(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder) { if (sortBy.Count == 0) { - return Array.Empty<(string, SortOrder)>(); + return Array.Empty<(ItemSortBy, SortOrder)>(); } - var result = new (string, SortOrder)[sortBy.Count]; + var result = new (ItemSortBy, SortOrder)[sortBy.Count]; var i = 0; // Add elements which have a SortOrder specified for (; i < requestedSortOrder.Count; i++) diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 5e7dd689e..6a30de5e6 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -107,7 +107,7 @@ public class GetProgramsDto /// Optional. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList SortBy { get; set; } = Array.Empty(); + public IReadOnlyList SortBy { get; set; } = Array.Empty(); /// /// Gets or sets sort Order - Ascending,Descending. diff --git a/Jellyfin.Data/Enums/ItemSortBy.cs b/Jellyfin.Data/Enums/ItemSortBy.cs new file mode 100644 index 000000000..17bf1166d --- /dev/null +++ b/Jellyfin.Data/Enums/ItemSortBy.cs @@ -0,0 +1,167 @@ +namespace Jellyfin.Data.Enums; + +/// +/// These represent sort orders. +/// +public enum ItemSortBy +{ + /// + /// Default sort order. + /// + Default = 0, + + /// + /// The aired episode order. + /// + AiredEpisodeOrder = 1, + + /// + /// The album. + /// + Album = 2, + + /// + /// The album artist. + /// + AlbumArtist = 3, + + /// + /// The artist. + /// + Artist = 4, + + /// + /// The date created. + /// + DateCreated = 5, + + /// + /// The official rating. + /// + OfficialRating = 6, + + /// + /// The date played. + /// + DatePlayed = 7, + + /// + /// The premiere date. + /// + PremiereDate = 8, + + /// + /// The start date. + /// + StartDate = 9, + + /// + /// The sort name. + /// + SortName = 10, + + /// + /// The name. + /// + Name = 11, + + /// + /// The random. + /// + Random = 12, + + /// + /// The runtime. + /// + Runtime = 13, + + /// + /// The community rating. + /// + CommunityRating = 14, + + /// + /// The production year. + /// + ProductionYear = 15, + + /// + /// The play count. + /// + PlayCount = 16, + + /// + /// The critic rating. + /// + CriticRating = 17, + + /// + /// The IsFolder boolean. + /// + IsFolder = 18, + + /// + /// The IsUnplayed boolean. + /// + IsUnplayed = 19, + + /// + /// The IsPlayed boolean. + /// + IsPlayed = 20, + + /// + /// The series sort. + /// + SeriesSortName = 21, + + /// + /// The video bitrate. + /// + VideoBitRate = 22, + + /// + /// The air time. + /// + AirTime = 23, + + /// + /// The studio. + /// + Studio = 24, + + /// + /// The IsFavouriteOrLiked boolean. + /// + IsFavoriteOrLiked = 25, + + /// + /// The last content added date. + /// + DateLastContentAdded = 26, + + /// + /// The series last played date. + /// + SeriesDatePlayed = 27, + + /// + /// The parent index number. + /// + ParentIndexNumber = 28, + + /// + /// The index number. + /// + IndexNumber = 29, + + /// + /// The similarity score. + /// + SimilarityScore = 30, + + /// + /// The search score. + /// + SearchScore = 31, +} diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs index 14459624e..7a73f3eaf 100644 --- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs +++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs @@ -1,5 +1,7 @@ #nullable disable +using Jellyfin.Data.Enums; + namespace MediaBrowser.Controller.Entities { /// diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index a51299284..fb50a4546 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.Controller.Entities MediaTypes = Array.Empty(); MinSimilarityScore = 20; OfficialRatings = Array.Empty(); - OrderBy = Array.Empty<(string, SortOrder)>(); + OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); PersonIds = Array.Empty(); PersonTypes = Array.Empty(); PresetViews = Array.Empty(); @@ -284,7 +284,7 @@ namespace MediaBrowser.Controller.Entities public bool? HasChapterImages { get; set; } - public IReadOnlyList<(string OrderBy, SortOrder SortOrder)> OrderBy { get; set; } + public IReadOnlyList<(ItemSortBy OrderBy, SortOrder SortOrder)> OrderBy { get; set; } public DateTime? MinDateCreated { get; set; } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 66210cb6c..d7ccfd8ae 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.Entities.Movies { public BoxSet() { - DisplayOrder = ItemSortBy.PremiereDate; + DisplayOrder = "PremiereDate"; } [JsonIgnore] @@ -116,13 +116,13 @@ namespace MediaBrowser.Controller.Entities.Movies { var children = base.GetChildren(user, includeLinkedChildren, query); - if (string.Equals(DisplayOrder, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(DisplayOrder, "SortName", StringComparison.OrdinalIgnoreCase)) { // Sort by name return LibraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); } - if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase)) { // Sort by release date return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); @@ -136,7 +136,7 @@ namespace MediaBrowser.Controller.Entities.Movies { var children = base.GetRecursiveChildren(user, query); - if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase)) { // Sort by release date return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList(); diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index f34e3d68d..52d546a4f 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -199,9 +199,9 @@ namespace MediaBrowser.Controller.Library /// The sort by. /// The sort order. /// IEnumerable{BaseItem}. - IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder); + IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder); - IEnumerable Sort(IEnumerable items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy); + IEnumerable Sort(IEnumerable items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy); /// /// Gets the user root folder. diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index c4ad352a3..2f7ebb5cc 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; namespace MediaBrowser.Controller.Providers { diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs index 07fe1ea8a..96f8a2af5 100644 --- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Sorting @@ -9,9 +10,8 @@ namespace MediaBrowser.Controller.Sorting public interface IBaseItemComparer : IComparer { /// - /// Gets the name. + /// Gets the comparer type. /// - /// The name. - string Name { get; } + ItemSortBy Type { get; } } } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index f913b2320..5a7193079 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs index 673d97a9e..d872572b7 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Model.LiveTv public LiveTvChannelQuery() { EnableUserData = true; - SortBy = Array.Empty(); + SortBy = Array.Empty(); } /// @@ -99,7 +99,7 @@ namespace MediaBrowser.Model.LiveTv public bool? IsSeries { get; set; } - public string[] SortBy { get; set; } + public ItemSortBy[] SortBy { get; set; } /// /// Gets or sets the sort order to return results with. diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs deleted file mode 100644 index 1a7c9a63b..000000000 --- a/MediaBrowser.Model/Querying/ItemSortBy.cs +++ /dev/null @@ -1,163 +0,0 @@ -namespace MediaBrowser.Model.Querying -{ - /// - /// These represent sort orders that are known by the core. - /// - public static class ItemSortBy - { - /// - /// The aired episode order. - /// - public const string AiredEpisodeOrder = "AiredEpisodeOrder"; - - /// - /// The album. - /// - public const string Album = "Album"; - - /// - /// The album artist. - /// - public const string AlbumArtist = "AlbumArtist"; - - /// - /// The artist. - /// - public const string Artist = "Artist"; - - /// - /// The date created. - /// - public const string DateCreated = "DateCreated"; - - /// - /// The official rating. - /// - public const string OfficialRating = "OfficialRating"; - - /// - /// The date played. - /// - public const string DatePlayed = "DatePlayed"; - - /// - /// The premiere date. - /// - public const string PremiereDate = "PremiereDate"; - - /// - /// The start date. - /// - public const string StartDate = "StartDate"; - - /// - /// The sort name. - /// - public const string SortName = "SortName"; - - /// - /// The name. - /// - public const string Name = "Name"; - - /// - /// The random. - /// - public const string Random = "Random"; - - /// - /// The runtime. - /// - public const string Runtime = "Runtime"; - - /// - /// The community rating. - /// - public const string CommunityRating = "CommunityRating"; - - /// - /// The production year. - /// - public const string ProductionYear = "ProductionYear"; - - /// - /// The play count. - /// - public const string PlayCount = "PlayCount"; - - /// - /// The critic rating. - /// - public const string CriticRating = "CriticRating"; - - /// - /// The IsFolder boolean. - /// - public const string IsFolder = "IsFolder"; - - /// - /// The IsUnplayed boolean. - /// - public const string IsUnplayed = "IsUnplayed"; - - /// - /// The IsPlayed boolean. - /// - public const string IsPlayed = "IsPlayed"; - - /// - /// The series sort. - /// - public const string SeriesSortName = "SeriesSortName"; - - /// - /// The video bitrate. - /// - public const string VideoBitRate = "VideoBitRate"; - - /// - /// The air time. - /// - public const string AirTime = "AirTime"; - - /// - /// The studio. - /// - public const string Studio = "Studio"; - - /// - /// The IsFavouriteOrLiked boolean. - /// - public const string IsFavoriteOrLiked = "IsFavoriteOrLiked"; - - /// - /// The last content added date. - /// - public const string DateLastContentAdded = "DateLastContentAdded"; - - /// - /// The series last played date. - /// - public const string SeriesDatePlayed = "SeriesDatePlayed"; - - /// - /// The parent index number. - /// - public const string ParentIndexNumber = "ParentIndexNumber"; - - /// - /// The index number. - /// - public const string IndexNumber = "IndexNumber"; - - /// - /// The similarity score. - /// - public const string SimilarityScore = "SimilarityScore"; - - /// - /// The search score. - /// - public const string SearchScore = "SearchScore"; - } -} diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index e336c8825..06445c90d 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index 500ebaf71..72e59c9ac 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs index 2d7741d81..a2d1b3607 100644 --- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs +++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs @@ -14,7 +14,7 @@ namespace Jellyfin.Api.Tests.Helpers { [Theory] [MemberData(nameof(GetOrderBy_Success_TestData))] - public static void GetOrderBy_Success(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder, (string, SortOrder)[] expected) + public static void GetOrderBy_Success(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder, (ItemSortBy, SortOrder)[] expected) { Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder)); } @@ -95,42 +95,42 @@ namespace Jellyfin.Api.Tests.Helpers Assert.Throws(() => RequestHelpers.GetUserId(principal, requestUserId)); } - public static TheoryData, IReadOnlyList, (string, SortOrder)[]> GetOrderBy_Success_TestData() + public static TheoryData, IReadOnlyList, (ItemSortBy, SortOrder)[]> GetOrderBy_Success_TestData() { - var data = new TheoryData, IReadOnlyList, (string, SortOrder)[]>(); + var data = new TheoryData, IReadOnlyList, (ItemSortBy, SortOrder)[]>(); data.Add( - Array.Empty(), + Array.Empty(), Array.Empty(), - Array.Empty<(string, SortOrder)>()); + Array.Empty<(ItemSortBy, SortOrder)>()); data.Add( - new string[] + new[] { - "IsFavoriteOrLiked", - "Random" + ItemSortBy.IsFavoriteOrLiked, + ItemSortBy.Random }, Array.Empty(), - new (string, SortOrder)[] + new (ItemSortBy, SortOrder)[] { - ("IsFavoriteOrLiked", SortOrder.Ascending), - ("Random", SortOrder.Ascending), + (ItemSortBy.IsFavoriteOrLiked, SortOrder.Ascending), + (ItemSortBy.Random, SortOrder.Ascending), }); data.Add( - new string[] + new[] { - "SortName", - "ProductionYear" + ItemSortBy.SortName, + ItemSortBy.ProductionYear }, - new SortOrder[] + new[] { SortOrder.Descending }, - new (string, SortOrder)[] + new (ItemSortBy, SortOrder)[] { - ("SortName", SortOrder.Descending), - ("ProductionYear", SortOrder.Descending), + (ItemSortBy.SortName, SortOrder.Descending), + (ItemSortBy.ProductionYear, SortOrder.Descending), }); return data; -- cgit v1.2.3 From 906f701fa81c7cde1a9a01b066f3f29ff98552a5 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Thu, 9 Nov 2023 14:00:29 -0700 Subject: Convert CollectionType, SpecialFolderType to enum (#9764) * Convert CollectionType, SpecialFolderType to enum * Hide internal enum CollectionType values * Apply suggestions from code review Co-authored-by: Shadowghost * Fix recent change * Update Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs Co-authored-by: Patrick Barron --------- Co-authored-by: Shadowghost Co-authored-by: Patrick Barron --- Emby.Dlna/ContentDirectory/ControlHandler.cs | 34 ++--- .../Images/CollectionFolderImageProvider.cs | 60 ++++---- .../Images/DynamicImageProvider.cs | 6 +- .../Library/LibraryManager.cs | 69 +++++---- .../Library/Resolvers/Audio/AudioResolver.cs | 14 +- .../Library/Resolvers/Audio/MusicAlbumResolver.cs | 3 +- .../Library/Resolvers/Audio/MusicArtistResolver.cs | 3 +- .../Library/Resolvers/Books/BookResolver.cs | 3 +- .../Library/Resolvers/Movies/MovieResolver.cs | 62 ++++---- .../Library/Resolvers/PhotoAlbumResolver.cs | 5 +- .../Library/Resolvers/PhotoResolver.cs | 5 +- .../Library/Resolvers/PlaylistResolver.cs | 7 +- .../Library/Resolvers/SpecialFolderResolver.cs | 6 +- .../Library/Resolvers/TV/EpisodeResolver.cs | 7 +- .../Library/Resolvers/TV/SeriesResolver.cs | 7 +- .../Library/UserViewManager.cs | 35 +++-- .../Playlists/PlaylistsFolder.cs | 2 +- Jellyfin.Api/Controllers/GenresController.cs | 4 +- Jellyfin.Api/Controllers/ItemUpdateController.cs | 9 +- Jellyfin.Api/Controllers/ItemsController.cs | 4 +- Jellyfin.Api/Controllers/LibraryController.cs | 4 +- Jellyfin.Api/Controllers/UserViewsController.cs | 3 +- .../Attributes/OpenApiIgnoreEnumAttribute.cs | 11 ++ Jellyfin.Data/Enums/CollectionType.cs | 164 +++++++++++++++++++++ .../Extensions/ApiServiceCollectionExtensions.cs | 1 + Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs | 42 ++++++ MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- .../Entities/BasePluginFolder.cs | 3 +- .../Entities/CollectionFolder.cs | 3 +- .../Entities/ICollectionFolder.cs | 3 +- .../Entities/InternalItemsQuery.cs | 4 +- MediaBrowser.Controller/Entities/UserView.cs | 37 ++--- .../Entities/UserViewBuilder.cs | 78 +++++----- MediaBrowser.Controller/Library/ILibraryManager.cs | 20 +-- .../Library/IUserViewManager.cs | 3 +- MediaBrowser.Controller/Library/ItemResolveArgs.cs | 7 +- MediaBrowser.Controller/Resolvers/IItemResolver.cs | 3 +- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- MediaBrowser.Model/Dto/MetadataEditorInfo.cs | 3 +- MediaBrowser.Model/Entities/CollectionType.cs | 27 ---- MediaBrowser.Model/Library/UserViewQuery.cs | 5 +- .../Library/AudioResolverTests.cs | 3 +- .../Library/EpisodeResolverTest.cs | 1 + 43 files changed, 486 insertions(+), 288 deletions(-) create mode 100644 Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs create mode 100644 Jellyfin.Data/Enums/CollectionType.cs create mode 100644 Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs delete mode 100644 MediaBrowser.Model/Entities/CollectionType.cs (limited to 'MediaBrowser.Model') diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index e685d252e..7d53263a6 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -565,30 +565,18 @@ namespace Emby.Dlna.ContentDirectory if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder) { - var collectionType = collectionFolder.CollectionType; - if (string.Equals(CollectionType.Music, collectionType, StringComparison.OrdinalIgnoreCase)) + switch (collectionFolder.CollectionType) { - return GetMusicFolders(item, user, stubType, sort, startIndex, limit); - } - - if (string.Equals(CollectionType.Movies, collectionType, StringComparison.OrdinalIgnoreCase)) - { - return GetMovieFolders(item, user, stubType, sort, startIndex, limit); - } - - if (string.Equals(CollectionType.TvShows, collectionType, StringComparison.OrdinalIgnoreCase)) - { - return GetTvFolders(item, user, stubType, sort, startIndex, limit); - } - - if (string.Equals(CollectionType.Folders, collectionType, StringComparison.OrdinalIgnoreCase)) - { - return GetFolders(user, startIndex, limit); - } - - if (string.Equals(CollectionType.LiveTv, collectionType, StringComparison.OrdinalIgnoreCase)) - { - return GetLiveTvChannels(user, sort, startIndex, limit); + case CollectionType.Music: + return GetMusicFolders(item, user, stubType, sort, startIndex, limit); + case CollectionType.Movies: + return GetMovieFolders(item, user, stubType, sort, startIndex, limit); + case CollectionType.TvShows: + return GetTvFolders(item, user, stubType, sort, startIndex, limit); + case CollectionType.Folders: + return GetFolders(user, startIndex, limit); + case CollectionType.LiveTv: + return GetLiveTvChannels(user, sort, startIndex, limit); } } diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 8a0e627b9..6e8f77977 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -30,47 +30,43 @@ namespace Emby.Server.Implementations.Images BaseItemKind[] includeItemTypes; - if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal)) + switch (viewType) { - includeItemTypes = new[] { BaseItemKind.Movie }; - } - else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.Series }; - } - else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.MusicAlbum }; - } - else if (string.Equals(viewType, CollectionType.MusicVideos, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.MusicVideo }; - } - else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook }; - } - else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.BoxSet }; - } - else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal)) - { - includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo }; - } - else - { - includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series }; + case CollectionType.Movies: + includeItemTypes = new[] { BaseItemKind.Movie }; + break; + case CollectionType.TvShows: + includeItemTypes = new[] { BaseItemKind.Series }; + break; + case CollectionType.Music: + includeItemTypes = new[] { BaseItemKind.MusicAlbum }; + break; + case CollectionType.MusicVideos: + includeItemTypes = new[] { BaseItemKind.MusicVideo }; + break; + case CollectionType.Books: + includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook }; + break; + case CollectionType.BoxSets: + includeItemTypes = new[] { BaseItemKind.BoxSet }; + break; + case CollectionType.HomeVideos: + case CollectionType.Photos: + includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo }; + break; + default: + includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series }; + break; } - var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase); + var recursive = viewType != CollectionType.Playlists; return view.GetItemList(new InternalItemsQuery { CollapseBoxSetItems = false, Recursive = recursive, DtoOptions = new DtoOptions(false), - ImageTypes = new ImageType[] { ImageType.Primary }, + ImageTypes = new[] { ImageType.Primary }, Limit = 8, OrderBy = new[] { diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 0bd5fdce0..5de53df73 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images var view = (UserView)item; var isUsingCollectionStrip = IsUsingCollectionStrip(view); - var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.BoxSets && view.ViewType != CollectionType.Playlists; var result = view.GetItemList(new InternalItemsQuery { @@ -112,14 +112,14 @@ namespace Emby.Server.Implementations.Images private static bool IsUsingCollectionStrip(UserView view) { - string[] collectionStripViewTypes = + CollectionType[] collectionStripViewTypes = { CollectionType.Movies, CollectionType.TvShows, CollectionType.Playlists }; - return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); + return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value); } protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 5c76e77be..f40177fa7 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -525,14 +525,14 @@ namespace Emby.Server.Implementations.Library IDirectoryService directoryService, IItemResolver[] resolvers, Folder parent = null, - string collectionType = null, + CollectionType? collectionType = null, LibraryOptions libraryOptions = null) { ArgumentNullException.ThrowIfNull(fileInfo); var fullPath = fileInfo.FullName; - if (string.IsNullOrEmpty(collectionType) && parent is not null) + if (collectionType is null && parent is not null) { collectionType = GetContentTypeOverride(fullPath, true); } @@ -635,7 +635,7 @@ namespace Emby.Server.Implementations.Library return !args.ContainsFileSystemEntryByName(".ignore"); } - public IEnumerable ResolvePaths(IEnumerable files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null) + public IEnumerable ResolvePaths(IEnumerable files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null) { return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers); } @@ -645,7 +645,7 @@ namespace Emby.Server.Implementations.Library IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, - string collectionType, + CollectionType? collectionType, IItemResolver[] resolvers) { var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList(); @@ -675,7 +675,7 @@ namespace Emby.Server.Implementations.Library IReadOnlyList fileList, IDirectoryService directoryService, Folder parent, - string collectionType, + CollectionType? collectionType, IItemResolver[] resolvers, LibraryOptions libraryOptions) { @@ -1514,7 +1514,7 @@ namespace Emby.Server.Implementations.Library { if (item is UserView view) { - if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.Ordinal)) + if (view.ViewType == CollectionType.LiveTv) { return new[] { view.Id }; } @@ -1543,13 +1543,13 @@ namespace Emby.Server.Implementations.Library } // Handle grouping - if (user is not null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) + if (user is not null && view.ViewType != CollectionType.Unknown && UserView.IsEligibleForGrouping(view.ViewType) && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0) { return GetUserRootFolder() .GetChildren(user, true) .OfType() - .Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) + .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType) .Where(i => user.IsFolderGrouped(i.Id)) .SelectMany(i => GetTopParentIdsForQuery(i, user)); } @@ -2065,16 +2065,16 @@ namespace Emby.Server.Implementations.Library : collectionFolder.GetLibraryOptions(); } - public string GetContentType(BaseItem item) + public CollectionType? GetContentType(BaseItem item) { - string configuredContentType = GetConfiguredContentType(item, false); - if (!string.IsNullOrEmpty(configuredContentType)) + var configuredContentType = GetConfiguredContentType(item, false); + if (configuredContentType is not null) { return configuredContentType; } configuredContentType = GetConfiguredContentType(item, true); - if (!string.IsNullOrEmpty(configuredContentType)) + if (configuredContentType is not null) { return configuredContentType; } @@ -2082,31 +2082,31 @@ namespace Emby.Server.Implementations.Library return GetInheritedContentType(item); } - public string GetInheritedContentType(BaseItem item) + public CollectionType? GetInheritedContentType(BaseItem item) { var type = GetTopFolderContentType(item); - if (!string.IsNullOrEmpty(type)) + if (type is not null) { return type; } return item.GetParents() .Select(GetConfiguredContentType) - .LastOrDefault(i => !string.IsNullOrEmpty(i)); + .LastOrDefault(i => i is not null); } - public string GetConfiguredContentType(BaseItem item) + public CollectionType? GetConfiguredContentType(BaseItem item) { return GetConfiguredContentType(item, false); } - public string GetConfiguredContentType(string path) + public CollectionType? GetConfiguredContentType(string path) { return GetContentTypeOverride(path, false); } - public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath) + public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath) { if (item is ICollectionFolder collectionFolder) { @@ -2116,16 +2116,21 @@ namespace Emby.Server.Implementations.Library return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath); } - private string GetContentTypeOverride(string path, bool inherit) + private CollectionType? GetContentTypeOverride(string path, bool inherit) { var nameValuePair = _configurationManager.Configuration.ContentTypes .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); - return nameValuePair?.Value; + if (Enum.TryParse(nameValuePair?.Value, out var collectionType)) + { + return collectionType; + } + + return null; } - private string GetTopFolderContentType(BaseItem item) + private CollectionType? GetTopFolderContentType(BaseItem item) { if (item is null) { @@ -2147,13 +2152,13 @@ namespace Emby.Server.Implementations.Library .OfType() .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path)) .Select(i => i.CollectionType) - .FirstOrDefault(i => !string.IsNullOrEmpty(i)); + .FirstOrDefault(i => i is not null); } public UserView GetNamedView( User user, string name, - string viewType, + CollectionType? viewType, string sortName) { return GetNamedView(user, name, Guid.Empty, viewType, sortName); @@ -2161,13 +2166,13 @@ namespace Emby.Server.Implementations.Library public UserView GetNamedView( string name, - string viewType, + CollectionType viewType, string sortName) { var path = Path.Combine( _configurationManager.ApplicationPaths.InternalMetadataPath, "views", - _fileSystem.GetValidFilename(viewType)); + _fileSystem.GetValidFilename(viewType.ToString())); var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView)); @@ -2207,13 +2212,13 @@ namespace Emby.Server.Implementations.Library User user, string name, Guid parentId, - string viewType, + CollectionType? viewType, string sortName) { var parentIdString = parentId.Equals(default) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); - var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); + var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -2269,7 +2274,7 @@ namespace Emby.Server.Implementations.Library public UserView GetShadowView( BaseItem parent, - string viewType, + CollectionType? viewType, string sortName) { ArgumentNullException.ThrowIfNull(parent); @@ -2277,7 +2282,7 @@ namespace Emby.Server.Implementations.Library var name = parent.Name; var parentId = parent.Id; - var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty); + var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -2334,7 +2339,7 @@ namespace Emby.Server.Implementations.Library public UserView GetNamedView( string name, Guid parentId, - string viewType, + CollectionType? viewType, string sortName, string uniqueId) { @@ -2343,7 +2348,7 @@ namespace Emby.Server.Implementations.Library var parentIdString = parentId.Equals(default) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); - var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); + var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty); if (!string.IsNullOrEmpty(uniqueId)) { idValues += uniqueId; @@ -2378,7 +2383,7 @@ namespace Emby.Server.Implementations.Library isNew = true; } - if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) + if (viewType != item.ViewType) { item.ViewType = viewType; item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 862f144e6..ac423ed09 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -10,11 +10,11 @@ using Emby.Naming.Audio; using Emby.Naming.AudioBook; using Emby.Naming.Common; using Emby.Naming.Video; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library.Resolvers.Audio @@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio public MultiItemResolverResult ResolveMultiple( Folder parent, List files, - string collectionType, + CollectionType? collectionType, IDirectoryService directoryService) { var result = ResolveMultipleInternal(parent, files, collectionType); @@ -59,9 +59,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio private MultiItemResolverResult ResolveMultipleInternal( Folder parent, List files, - string collectionType) + CollectionType? collectionType) { - if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.Books) { return ResolveMultipleAudio(parent, files, true); } @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var collectionType = args.GetCollectionType(); - var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase); + var isBooksCollectionType = collectionType == CollectionType.Books; if (args.IsDirectory) { @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } - var isMixedCollectionType = string.IsNullOrEmpty(collectionType); + var isMixedCollectionType = collectionType is null; // For conflicting extensions, give priority to videos if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions)) @@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio MediaBrowser.Controller.Entities.Audio.Audio item = null; - var isMusicCollectionType = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase); + var isMusicCollectionType = collectionType == CollectionType.Music; // Use regular audio type for mixed libraries, owned items and music if (isMixedCollectionType || diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index bbc70701c..06e292f4c 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Naming.Audio; using Emby.Naming.Common; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -54,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio protected override MusicAlbum Resolve(ItemResolveArgs args) { var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase); + var isMusicMediaFolder = collectionType == CollectionType.Music; // If there's a collection type and it's not music, don't allow it. if (!isMusicMediaFolder) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index c858dc53d..7d6f97b12 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Emby.Naming.Common; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase); + var isMusicMediaFolder = collectionType == CollectionType.Music; // If there's a collection type and it's not music, it can't be a music artist if (!isMusicMediaFolder) diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 73861ff59..b76bfe427 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var collectionType = args.GetCollectionType(); // Only process items that are in a collection folder containing books - if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase)) + if (collectionType != CollectionType.Books) { return null; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 0b65bf921..50fd8b877 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Common; using Emby.Naming.Video; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -28,13 +29,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { private readonly IImageProcessor _imageProcessor; - private string[] _validCollectionTypes = new[] + private static readonly CollectionType[] _validCollectionTypes = new[] { - CollectionType.Movies, - CollectionType.HomeVideos, - CollectionType.MusicVideos, - CollectionType.TvShows, - CollectionType.Photos + CollectionType.Movies, + CollectionType.HomeVideos, + CollectionType.MusicVideos, + CollectionType.TvShows, + CollectionType.Photos }; /// @@ -63,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies public MultiItemResolverResult ResolveMultiple( Folder parent, List files, - string collectionType, + CollectionType? collectionType, IDirectoryService directoryService) { var result = ResolveMultipleInternal(parent, files, collectionType); @@ -99,17 +100,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Video movie = null; var files = args.GetActualFileSystemChildren().ToList(); - if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.MusicVideos) { movie = FindMovie(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } - if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.HomeVideos) { movie = FindMovie /// Movie. - private T FindMovie(ItemResolveArgs args, string path, Folder parent, List fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName) + private T FindMovie(ItemResolveArgs args, string path, Folder parent, List fileSystemEntries, IDirectoryService directoryService, CollectionType? collectionType, bool parseName) where T : Video, new() { var multiDiscFolders = new List(); var libraryOptions = args.LibraryOptions; - var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos; + var supportPhotos = collectionType == CollectionType.HomeVideos && libraryOptions.EnablePhotos; var photos = new List(); // Search for a folder rip @@ -460,8 +459,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var result = ResolveVideos(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ?? new MultiItemResolverResult(); - var isPhotosCollection = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) - || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase); + var isPhotosCollection = collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos; if (!isPhotosCollection && result.Items.Count == 1) { var videoPath = result.Items[0].Path; @@ -562,7 +560,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return returnVideo; } - private bool IsInvalid(Folder parent, ReadOnlySpan collectionType) + private bool IsInvalid(Folder parent, CollectionType? collectionType) { if (parent is not null) { @@ -572,12 +570,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - if (collectionType.IsEmpty) + if (collectionType is null) { return false; } - return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase); + return !_validCollectionTypes.Contains(collectionType.Value); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs index 7dd0ab185..29d540700 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs @@ -2,6 +2,7 @@ using System; using Emby.Naming.Common; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -45,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.GetCollectionType(); - if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) - || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos)) + if (collectionType == CollectionType.Photos + || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) { if (HasPhotos(args)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index c860391fc..d166ac37f 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using Emby.Naming.Common; using Emby.Naming.Video; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -60,8 +61,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.CollectionType; - if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) - || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos)) + if (collectionType == CollectionType.Photos + || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) { if (IsImageFile(args.Path, _imageProcessor)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 5d569009d..d4b3722c9 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; @@ -19,9 +20,9 @@ namespace Emby.Server.Implementations.Library.Resolvers /// public class PlaylistResolver : GenericFolderResolver { - private string[] _musicPlaylistCollectionTypes = + private CollectionType?[] _musicPlaylistCollectionTypes = { - string.Empty, + null, CollectionType.Music }; @@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers // Check if this is a music playlist file // It should have the correct collection type and a supported file extension - else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType)) { var extension = Path.GetExtension(args.Path.AsSpan()); if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs index 6bb999641..3d91ed242 100644 --- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - private string GetCollectionType(ItemResolveArgs args) + private CollectionType? GetCollectionType(ItemResolveArgs args) { return args.FileSystemChildren .Where(i => @@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Library.Resolvers } }) .Select(i => _fileSystem.GetFileNameWithoutExtension(i)) - .FirstOrDefault(); + .Select(i => Enum.TryParse(i, out var collectionType) ? collectionType : (CollectionType?)null) + .FirstOrDefault(i => i is not null); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 392ee4c77..8274881be 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using Emby.Naming.Common; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -48,9 +49,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something // Also handle flat tv folders - if (season is not null || - string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || - args.HasParent()) + if (season is not null + || args.GetCollectionType() == CollectionType.TvShows + || args.HasParent()) { var episode = ResolveVideo(args, false); diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index d4f275bed..2ae1138a5 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -8,6 +8,7 @@ using System.IO; using Emby.Naming.Common; using Emby.Naming.TV; using Emby.Naming.Video; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -59,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path); var collectionType = args.GetCollectionType(); - if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.TvShows) { // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType var configuredContentType = args.GetConfiguredContentType(); - if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + if (configuredContentType != CollectionType.TvShows) { return new Series { @@ -72,7 +73,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV }; } } - else if (string.IsNullOrEmpty(collectionType)) + else if (collectionType is null) { if (args.ContainsFileSystemEntryByName("tvshow.nfo")) { diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 2c3dc1857..df5610996 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Threading; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -64,8 +63,8 @@ namespace Emby.Server.Implementations.Library var collectionFolder = folder as ICollectionFolder; var folderViewType = collectionFolder?.CollectionType; - // Playlist library requires special handling because the folder only refrences user playlists - if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + // Playlist library requires special handling because the folder only references user playlists + if (folderViewType == CollectionType.Playlists) { var items = folder.GetItemList(new InternalItemsQuery(user) { @@ -90,7 +89,7 @@ namespace Emby.Server.Implementations.Library continue; } - if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (query.PresetViews.Contains(folderViewType)) { list.Add(GetUserView(folder, folderViewType, string.Empty)); } @@ -102,14 +101,14 @@ namespace Emby.Server.Implementations.Library foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows }) { - var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(i.CollectionType)) + var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null) .ToList(); if (parents.Count > 0) { - var localizationKey = string.Equals(viewType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ? - "TvShows" : - "Movies"; + var localizationKey = viewType == CollectionType.TvShows + ? "TvShows" + : "Movies"; list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews)); } @@ -164,14 +163,14 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public UserView GetUserSubViewWithName(string name, Guid parentId, string type, string sortName) + public UserView GetUserSubViewWithName(string name, Guid parentId, CollectionType? type, string sortName) { var uniqueId = parentId + "subview" + type; return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId); } - public UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName) + public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName) { var name = _localizationManager.GetLocalizedString(localizationKey); @@ -180,15 +179,15 @@ namespace Emby.Server.Implementations.Library private Folder GetUserView( List parents, - string viewType, + CollectionType? viewType, string localizationKey, string sortName, User user, - string[] presetViews) + CollectionType?[] presetViews) { - if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase))) + if (parents.Count == 1 && parents.All(i => i.CollectionType == viewType)) { - if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase)) + if (!presetViews.Contains(viewType)) { return (Folder)parents[0]; } @@ -200,7 +199,7 @@ namespace Emby.Server.Implementations.Library return _libraryManager.GetNamedView(user, name, viewType, sortName); } - public UserView GetUserView(Folder parent, string viewType, string sortName) + public UserView GetUserView(Folder parent, CollectionType? viewType, string sortName) { return _libraryManager.GetShadowView(parent, viewType, sortName); } @@ -280,7 +279,7 @@ namespace Emby.Server.Implementations.Library var isPlayed = request.IsPlayed; - if (parents.OfType().Any(i => string.Equals(i.CollectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase))) + if (parents.OfType().Any(i => i.CollectionType == CollectionType.Music)) { isPlayed = null; } @@ -306,11 +305,11 @@ namespace Emby.Server.Implementations.Library var hasCollectionType = parents.OfType().ToArray(); if (hasCollectionType.Length > 0) { - if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))) + if (hasCollectionType.All(i => i.CollectionType == CollectionType.Movies)) { includeItemTypes = new[] { BaseItemKind.Movie }; } - else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))) + else if (hasCollectionType.All(i => i.CollectionType == CollectionType.TvShows)) { includeItemTypes = new[] { BaseItemKind.Episode }; } diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index d67caa52d..5c616d534 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Playlists public override bool SupportsInheritedParentImages => false; [JsonIgnore] - public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists; + public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.Playlists; protected override IEnumerable GetEligibleChildrenForRecursiveChildren(User user) { diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 51f04fa27..062e1062d 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -131,8 +131,8 @@ public class GenresController : BaseJellyfinApiController QueryResult<(BaseItem, ItemCounts)> result; if (parentItem is ICollectionFolder parentCollectionFolder - && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal) - || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal))) + && (parentCollectionFolder.CollectionType == CollectionType.Music + || parentCollectionFolder.CollectionType == CollectionType.MusicVideos)) { result = _libraryManager.GetMusicGenres(query); } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 504f2fa1d..3be891b93 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -164,18 +165,16 @@ public class ItemUpdateController : BaseJellyfinApiController var inheritedContentType = _libraryManager.GetInheritedContentType(item); var configuredContentType = _libraryManager.GetConfiguredContentType(item); - if (string.IsNullOrWhiteSpace(inheritedContentType) || - !string.IsNullOrWhiteSpace(configuredContentType)) + if (inheritedContentType is null || configuredContentType is not null) { info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); info.ContentType = configuredContentType; - if (string.IsNullOrWhiteSpace(inheritedContentType) - || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + if (inheritedContentType is null || inheritedContentType == CollectionType.TvShows) { info.ContentTypeOptions = info.ContentTypeOptions .Where(i => string.IsNullOrWhiteSpace(i.Value) - || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase)) .ToArray(); } } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 891cf88a7..3920d6599 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -269,13 +269,13 @@ public class ItemsController : BaseJellyfinApiController folder = _libraryManager.GetUserRootFolder(); } - string? collectionType = null; + CollectionType? collectionType = null; if (folder is IHasCollectionType hasCollectionType) { collectionType = hasCollectionType.CollectionType; } - if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + if (collectionType == CollectionType.Playlists) { recursive = true; includeItemTypes = new[] { BaseItemKind.Playlist }; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 21941ff94..3cd78b086 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -788,7 +788,7 @@ public class LibraryController : BaseJellyfinApiController [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetLibraryOptionsInfo( - [FromQuery] string? libraryContentType, + [FromQuery] CollectionType? libraryContentType, [FromQuery] bool isNewLibrary = false) { var result = new LibraryOptionsResultDto(); @@ -922,7 +922,7 @@ public class LibraryController : BaseJellyfinApiController } } - private static string[] GetRepresentativeItemTypes(string? contentType) + private static string[] GetRepresentativeItemTypes(CollectionType? contentType) { return contentType switch { diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 838b43234..0ffa3ab1a 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -6,6 +6,7 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -63,7 +64,7 @@ public class UserViewsController : BaseJellyfinApiController public QueryResult GetUserViews( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews, [FromQuery] bool includeHidden = false) { var query = new UserViewQuery diff --git a/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs b/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs new file mode 100644 index 000000000..ff613d9f8 --- /dev/null +++ b/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Jellyfin.Data.Attributes; + +/// +/// Attribute to specify that the enum value is to be ignored when generating the openapi spec. +/// +[AttributeUsage(AttributeTargets.Field)] +public sealed class OpenApiIgnoreEnumAttribute : Attribute +{ +} diff --git a/Jellyfin.Data/Enums/CollectionType.cs b/Jellyfin.Data/Enums/CollectionType.cs new file mode 100644 index 000000000..e2044a0bc --- /dev/null +++ b/Jellyfin.Data/Enums/CollectionType.cs @@ -0,0 +1,164 @@ +using Jellyfin.Data.Attributes; + +namespace Jellyfin.Data.Enums; + +/// +/// Collection type. +/// +public enum CollectionType +{ + /// + /// Unknown collection. + /// + Unknown = 0, + + /// + /// Movies collection. + /// + Movies = 1, + + /// + /// Tv shows collection. + /// + TvShows = 2, + + /// + /// Music collection. + /// + Music = 3, + + /// + /// Music videos collection. + /// + MusicVideos = 4, + + /// + /// Trailers collection. + /// + Trailers = 5, + + /// + /// Home videos collection. + /// + HomeVideos = 6, + + /// + /// Box sets collection. + /// + BoxSets = 7, + + /// + /// Books collection. + /// + Books = 8, + + /// + /// Photos collection. + /// + Photos = 9, + + /// + /// Live tv collection. + /// + LiveTv = 10, + + /// + /// Playlists collection. + /// + Playlists = 11, + + /// + /// Folders collection. + /// + Folders = 12, + + /// + /// Tv show series collection. + /// + [OpenApiIgnoreEnum] + TvShowSeries = 101, + + /// + /// Tv genres collection. + /// + [OpenApiIgnoreEnum] + TvGenres = 102, + + /// + /// Tv genre collection. + /// + [OpenApiIgnoreEnum] + TvGenre = 103, + + /// + /// Tv latest collection. + /// + [OpenApiIgnoreEnum] + TvLatest = 104, + + /// + /// Tv next up collection. + /// + [OpenApiIgnoreEnum] + TvNextUp = 105, + + /// + /// Tv resume collection. + /// + [OpenApiIgnoreEnum] + TvResume = 106, + + /// + /// Tv favorite series collection. + /// + [OpenApiIgnoreEnum] + TvFavoriteSeries = 107, + + /// + /// Tv favorite episodes collection. + /// + [OpenApiIgnoreEnum] + TvFavoriteEpisodes = 108, + + /// + /// Latest movies collection. + /// + [OpenApiIgnoreEnum] + MovieLatest = 109, + + /// + /// Movies to resume collection. + /// + [OpenApiIgnoreEnum] + MovieResume = 110, + + /// + /// Movie movie collection. + /// + [OpenApiIgnoreEnum] + MovieMovies = 111, + + /// + /// Movie collections collection. + /// + [OpenApiIgnoreEnum] + MovieCollections = 112, + + /// + /// Movie favorites collection. + /// + [OpenApiIgnoreEnum] + MovieFavorites = 113, + + /// + /// Movie genres collection. + /// + [OpenApiIgnoreEnum] + MovieGenres = 114, + + /// + /// Movie genre collection. + /// + [OpenApiIgnoreEnum] + MovieGenre = 115 +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index b7e71a81d..16b58808f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -246,6 +246,7 @@ namespace Jellyfin.Server.Extensions // TODO - remove when all types are supported in System.Text.Json c.AddSwaggerTypeMappings(); + c.SchemaFilter(); c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); diff --git a/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs new file mode 100644 index 000000000..eb9ad03c2 --- /dev/null +++ b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Jellyfin.Data.Attributes; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +/// +/// Filter to remove ignored enum values. +/// +public class IgnoreEnumSchemaFilter : ISchemaFilter +{ + /// + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (context.Type.IsEnum || (Nullable.GetUnderlyingType(context.Type)?.IsEnum ?? false)) + { + var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type); + if (type is null) + { + return; + } + + var enumOpenApiStrings = new List(); + + foreach (var enumName in Enum.GetNames(type)) + { + var member = type.GetMember(enumName)[0]; + if (!member.GetCustomAttributes().Any()) + { + enumOpenApiStrings.Add(new OpenApiString(enumName)); + } + } + + schema.Enum = enumOpenApiStrings; + } + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 9f3e8eec9..87a021d41 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -724,7 +724,7 @@ namespace MediaBrowser.Controller.Entities if (this is IHasCollectionType view) { - if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) + if (view.CollectionType == CollectionType.LiveTv) { return true; } diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs index afafaf1c2..4bf21061c 100644 --- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; namespace MediaBrowser.Controller.Entities { @@ -11,7 +12,7 @@ namespace MediaBrowser.Controller.Entities public abstract class BasePluginFolder : Folder, ICollectionFolder { [JsonIgnore] - public virtual string? CollectionType => null; + public virtual CollectionType? CollectionType => null; [JsonIgnore] public override bool SupportsInheritedParentImages => false; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index f51162f9d..992bb19bb 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -11,6 +11,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; @@ -69,7 +70,7 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public override bool SupportsInheritedParentImages => false; - public string CollectionType { get; set; } + public CollectionType? CollectionType { get; set; } /// /// Gets the item's children. diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs index 89e494ebc..742691b00 100644 --- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -3,6 +3,7 @@ #pragma warning disable CA1819, CS1591 using System; +using Jellyfin.Data.Enums; namespace MediaBrowser.Controller.Entities { @@ -27,6 +28,6 @@ namespace MediaBrowser.Controller.Entities public interface IHasCollectionType { - string CollectionType { get; } + CollectionType? CollectionType { get; } } } diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index fb50a4546..c0bba60eb 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Controller.Entities OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); PersonIds = Array.Empty(); PersonTypes = Array.Empty(); - PresetViews = Array.Empty(); + PresetViews = Array.Empty(); SeriesStatuses = Array.Empty(); SourceTypes = Array.Empty(); StudioIds = Array.Empty(); @@ -248,7 +248,7 @@ namespace MediaBrowser.Controller.Entities public Guid[] TopParentIds { get; set; } - public string[] PresetViews { get; set; } + public CollectionType?[] PresetViews { get; set; } public TrailerType[] TrailerTypes { get; set; } diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 47432ee93..1f94cf767 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; @@ -16,21 +17,21 @@ namespace MediaBrowser.Controller.Entities { public class UserView : Folder, IHasCollectionType { - private static readonly string[] _viewTypesEligibleForGrouping = new string[] + private static readonly CollectionType?[] _viewTypesEligibleForGrouping = { - Model.Entities.CollectionType.Movies, - Model.Entities.CollectionType.TvShows, - string.Empty + Jellyfin.Data.Enums.CollectionType.Movies, + Jellyfin.Data.Enums.CollectionType.TvShows, + null }; - private static readonly string[] _originalFolderViewTypes = new string[] + private static readonly CollectionType?[] _originalFolderViewTypes = { - Model.Entities.CollectionType.Books, - Model.Entities.CollectionType.MusicVideos, - Model.Entities.CollectionType.HomeVideos, - Model.Entities.CollectionType.Photos, - Model.Entities.CollectionType.Music, - Model.Entities.CollectionType.BoxSets + Jellyfin.Data.Enums.CollectionType.Books, + Jellyfin.Data.Enums.CollectionType.MusicVideos, + Jellyfin.Data.Enums.CollectionType.HomeVideos, + Jellyfin.Data.Enums.CollectionType.Photos, + Jellyfin.Data.Enums.CollectionType.Music, + Jellyfin.Data.Enums.CollectionType.BoxSets }; public static ITVSeriesManager TVSeriesManager { get; set; } @@ -38,7 +39,7 @@ namespace MediaBrowser.Controller.Entities /// /// Gets or sets the view type. /// - public string ViewType { get; set; } + public CollectionType? ViewType { get; set; } /// /// Gets or sets the display parent id. @@ -52,7 +53,7 @@ namespace MediaBrowser.Controller.Entities /// [JsonIgnore] - public string CollectionType => ViewType; + public CollectionType? CollectionType => ViewType; /// [JsonIgnore] @@ -160,7 +161,7 @@ namespace MediaBrowser.Controller.Entities return true; } - return string.Equals(Model.Entities.CollectionType.Playlists, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase); + return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.Playlists; } public static bool IsEligibleForGrouping(Folder folder) @@ -169,14 +170,14 @@ namespace MediaBrowser.Controller.Entities && IsEligibleForGrouping(collectionFolder.CollectionType); } - public static bool IsEligibleForGrouping(string viewType) + public static bool IsEligibleForGrouping(CollectionType? viewType) { - return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return _viewTypesEligibleForGrouping.Contains(viewType); } - public static bool EnableOriginalFolder(string viewType) + public static bool EnableOriginalFolder(CollectionType? viewType) { - return _originalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return _originalFolderViewTypes.Contains(viewType); } protected override Task ValidateChildrenInternal(IProgress progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index c276ab463..a134735b2 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Controller.Entities _tvSeriesManager = tvSeriesManager; } - public QueryResult GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query) + public QueryResult GetUserItems(Folder queryParent, Folder displayParent, CollectionType? viewType, InternalItemsQuery query) { var user = query.User; @@ -67,49 +67,49 @@ namespace MediaBrowser.Controller.Entities case CollectionType.Movies: return GetMovieFolders(queryParent, user, query); - case SpecialFolder.TvShowSeries: + case CollectionType.TvShowSeries: return GetTvSeries(queryParent, user, query); - case SpecialFolder.TvGenres: + case CollectionType.TvGenres: return GetTvGenres(queryParent, user, query); - case SpecialFolder.TvGenre: + case CollectionType.TvGenre: return GetTvGenreItems(queryParent, displayParent, user, query); - case SpecialFolder.TvResume: + case CollectionType.TvResume: return GetTvResume(queryParent, user, query); - case SpecialFolder.TvNextUp: + case CollectionType.TvNextUp: return GetTvNextUp(queryParent, query); - case SpecialFolder.TvLatest: + case CollectionType.TvLatest: return GetTvLatest(queryParent, user, query); - case SpecialFolder.MovieFavorites: + case CollectionType.MovieFavorites: return GetFavoriteMovies(queryParent, user, query); - case SpecialFolder.MovieLatest: + case CollectionType.MovieLatest: return GetMovieLatest(queryParent, user, query); - case SpecialFolder.MovieGenres: + case CollectionType.MovieGenres: return GetMovieGenres(queryParent, user, query); - case SpecialFolder.MovieGenre: + case CollectionType.MovieGenre: return GetMovieGenreItems(queryParent, displayParent, user, query); - case SpecialFolder.MovieResume: + case CollectionType.MovieResume: return GetMovieResume(queryParent, user, query); - case SpecialFolder.MovieMovies: + case CollectionType.MovieMovies: return GetMovieMovies(queryParent, user, query); - case SpecialFolder.MovieCollections: + case CollectionType.MovieCollections: return GetMovieCollections(user, query); - case SpecialFolder.TvFavoriteEpisodes: + case CollectionType.TvFavoriteEpisodes: return GetFavoriteEpisodes(queryParent, user, query); - case SpecialFolder.TvFavoriteSeries: + case CollectionType.TvFavoriteSeries: return GetFavoriteSeries(queryParent, user, query); default: @@ -146,12 +146,12 @@ namespace MediaBrowser.Controller.Entities var list = new List { - GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent), - GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent), - GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent), - GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent), - GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent), - GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent) + GetUserView(CollectionType.MovieResume, "HeaderContinueWatching", "0", parent), + GetUserView(CollectionType.MovieLatest, "Latest", "1", parent), + GetUserView(CollectionType.MovieMovies, "Movies", "2", parent), + GetUserView(CollectionType.MovieCollections, "Collections", "3", parent), + GetUserView(CollectionType.MovieFavorites, "Favorites", "4", parent), + GetUserView(CollectionType.MovieGenres, "Genres", "5", parent) }; return GetResult(list, query); @@ -264,7 +264,7 @@ namespace MediaBrowser.Controller.Entities } }) .Where(i => i is not null) - .Select(i => GetUserViewWithName(SpecialFolder.MovieGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(CollectionType.MovieGenre, i.SortName, parent)); return GetResult(genres, query); } @@ -303,13 +303,13 @@ namespace MediaBrowser.Controller.Entities var list = new List { - GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent), - GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent), - GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent), - GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent), - GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), - GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), - GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent) + GetUserView(CollectionType.TvResume, "HeaderContinueWatching", "0", parent), + GetUserView(CollectionType.TvNextUp, "HeaderNextUp", "1", parent), + GetUserView(CollectionType.TvLatest, "Latest", "2", parent), + GetUserView(CollectionType.TvShowSeries, "Shows", "3", parent), + GetUserView(CollectionType.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), + GetUserView(CollectionType.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), + GetUserView(CollectionType.TvGenres, "Genres", "6", parent) }; return GetResult(list, query); @@ -330,7 +330,7 @@ namespace MediaBrowser.Controller.Entities private QueryResult GetTvNextUp(Folder parent, InternalItemsQuery query) { - var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty }); + var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows }); var result = _tvSeriesManager.GetNextUp( new NextUpQuery @@ -392,7 +392,7 @@ namespace MediaBrowser.Controller.Entities } }) .Where(i => i is not null) - .Select(i => GetUserViewWithName(SpecialFolder.TvGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(CollectionType.TvGenre, i.SortName, parent)); return GetResult(genres, query); } @@ -943,7 +943,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => user.IsFolderGrouped(i.Id) && UserView.IsEligibleForGrouping(i)); } - private BaseItem[] GetMediaFolders(User user, IEnumerable viewTypes) + private BaseItem[] GetMediaFolders(User user, IEnumerable viewTypes) { if (user is null) { @@ -952,7 +952,7 @@ namespace MediaBrowser.Controller.Entities { var folder = i as ICollectionFolder; - return folder is not null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return folder?.CollectionType is not null && viewTypes.Contains(folder.CollectionType.Value); }).ToArray(); } @@ -961,11 +961,11 @@ namespace MediaBrowser.Controller.Entities { var folder = i as ICollectionFolder; - return folder is not null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase); + return folder?.CollectionType is not null && viewTypes.Contains(folder.CollectionType.Value); }).ToArray(); } - private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable viewTypes) + private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable viewTypes) { if (parent is null || parent is UserView) { @@ -975,12 +975,12 @@ namespace MediaBrowser.Controller.Entities return new BaseItem[] { parent }; } - private UserView GetUserViewWithName(string type, string sortName, BaseItem parent) + private UserView GetUserViewWithName(CollectionType? type, string sortName, BaseItem parent) { - return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N", CultureInfo.InvariantCulture), type, sortName); + return _userViewManager.GetUserSubView(parent.Id, type, parent.Id.ToString("N", CultureInfo.InvariantCulture), sortName); } - private UserView GetUserView(string type, string localizationKey, string sortName, BaseItem parent) + private UserView GetUserView(CollectionType? type, string localizationKey, string sortName, BaseItem parent) { return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName); } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 52d546a4f..9ec22324f 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -79,7 +79,7 @@ namespace MediaBrowser.Controller.Library IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, - string collectionType = null); + CollectionType? collectionType = null); /// /// Gets a Person. @@ -256,28 +256,28 @@ namespace MediaBrowser.Controller.Library /// /// The item. /// System.String. - string GetContentType(BaseItem item); + CollectionType? GetContentType(BaseItem item); /// /// Gets the type of the inherited content. /// /// The item. /// System.String. - string GetInheritedContentType(BaseItem item); + CollectionType? GetInheritedContentType(BaseItem item); /// /// Gets the type of the configured content. /// /// The item. /// System.String. - string GetConfiguredContentType(BaseItem item); + CollectionType? GetConfiguredContentType(BaseItem item); /// /// Gets the type of the configured content. /// /// The path. /// System.String. - string GetConfiguredContentType(string path); + CollectionType? GetConfiguredContentType(string path); /// /// Normalizes the root path list. @@ -329,7 +329,7 @@ namespace MediaBrowser.Controller.Library User user, string name, Guid parentId, - string viewType, + CollectionType? viewType, string sortName); /// @@ -343,7 +343,7 @@ namespace MediaBrowser.Controller.Library UserView GetNamedView( User user, string name, - string viewType, + CollectionType? viewType, string sortName); /// @@ -355,7 +355,7 @@ namespace MediaBrowser.Controller.Library /// The named view. UserView GetNamedView( string name, - string viewType, + CollectionType viewType, string sortName); /// @@ -370,7 +370,7 @@ namespace MediaBrowser.Controller.Library UserView GetNamedView( string name, Guid parentId, - string viewType, + CollectionType? viewType, string sortName, string uniqueId); @@ -383,7 +383,7 @@ namespace MediaBrowser.Controller.Library /// The shadow view. UserView GetShadowView( BaseItem parent, - string viewType, + CollectionType? viewType, string sortName); /// diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs index 055627d3e..a565dc88b 100644 --- a/MediaBrowser.Controller/Library/IUserViewManager.cs +++ b/MediaBrowser.Controller/Library/IUserViewManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Library; @@ -28,7 +29,7 @@ namespace MediaBrowser.Controller.Library /// Localization key to use. /// Sort to use. /// User view. - UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName); + UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName); /// /// Gets latest items. diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index dcd0110fb..6202f92f5 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; @@ -120,7 +121,7 @@ namespace MediaBrowser.Controller.Library } } - public string CollectionType { get; set; } + public CollectionType? CollectionType { get; set; } public bool HasParent() where T : Folder @@ -220,7 +221,7 @@ namespace MediaBrowser.Controller.Library return GetFileSystemEntryByName(name) is not null; } - public string GetCollectionType() + public CollectionType? GetCollectionType() { return CollectionType; } @@ -229,7 +230,7 @@ namespace MediaBrowser.Controller.Library /// Gets the configured content type for the path. /// /// The configured content type. - public string GetConfiguredContentType() + public CollectionType? GetConfiguredContentType() { return _libraryManager.GetConfiguredContentType(Path); } diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index 282aa721e..0699734c4 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -32,7 +33,7 @@ namespace MediaBrowser.Controller.Resolvers MultiItemResolverResult ResolveMultiple( Folder parent, List files, - string collectionType, + CollectionType? collectionType, IDirectoryService directoryService); } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 287966dd0..709039fc5 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -420,7 +420,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the type of the collection. /// /// The type of the collection. - public string CollectionType { get; set; } + public CollectionType? CollectionType { get; set; } /// /// Gets or sets the display order. diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs index d098669ba..a3035bf61 100644 --- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs +++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Providers; @@ -27,7 +28,7 @@ namespace MediaBrowser.Model.Dto public IReadOnlyList ExternalIdInfos { get; set; } - public string? ContentType { get; set; } + public CollectionType? ContentType { get; set; } public IReadOnlyList ContentTypeOptions { get; set; } } diff --git a/MediaBrowser.Model/Entities/CollectionType.cs b/MediaBrowser.Model/Entities/CollectionType.cs deleted file mode 100644 index 60b69d4b0..000000000 --- a/MediaBrowser.Model/Entities/CollectionType.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Entities -{ - public static class CollectionType - { - public const string Movies = "movies"; - - public const string TvShows = "tvshows"; - - public const string Music = "music"; - - public const string MusicVideos = "musicvideos"; - - public const string Trailers = "trailers"; - - public const string HomeVideos = "homevideos"; - - public const string BoxSets = "boxsets"; - - public const string Books = "books"; - public const string Photos = "photos"; - public const string LiveTv = "livetv"; - public const string Playlists = "playlists"; - public const string Folders = "folders"; - } -} diff --git a/MediaBrowser.Model/Library/UserViewQuery.cs b/MediaBrowser.Model/Library/UserViewQuery.cs index 8a49b6863..e20d6af49 100644 --- a/MediaBrowser.Model/Library/UserViewQuery.cs +++ b/MediaBrowser.Model/Library/UserViewQuery.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Library { @@ -9,7 +10,7 @@ namespace MediaBrowser.Model.Library public UserViewQuery() { IncludeExternalContent = true; - PresetViews = Array.Empty(); + PresetViews = Array.Empty(); } /// @@ -30,6 +31,6 @@ namespace MediaBrowser.Model.Library /// true if [include hidden]; otherwise, false. public bool IncludeHidden { get; set; } - public string[] PresetViews { get; set; } + public CollectionType?[] PresetViews { get; set; } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs index d136c1bc6..16202aea9 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs @@ -1,6 +1,7 @@ using System.Linq; using Emby.Naming.Common; using Emby.Server.Implementations.Library.Resolvers.Audio; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; @@ -62,7 +63,7 @@ public class AudioResolverTests null, Mock.Of()) { - CollectionType = "books", + CollectionType = CollectionType.Books, FileInfo = new FileSystemMetadata { FullName = parent, diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index 6d0ed7bbb..92bac722b 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -1,5 +1,6 @@ using Emby.Naming.Common; using Emby.Server.Implementations.Library.Resolvers.TV; +using Jellyfin.Data.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; -- cgit v1.2.3 From 3fd505a4543a4ee42ead01793a91e0410032321b Mon Sep 17 00:00:00 2001 From: Chris H <70915190+Chris-Codes-It@users.noreply.github.com> Date: Fri, 10 Nov 2023 14:51:44 +0000 Subject: Validate AuthenticationProviderId and PasswordResetProviderId (#10553) --- CONTRIBUTORS.md | 3 +- MediaBrowser.Model/Users/UserPolicy.cs | 3 + .../Controllers/UserControllerTests.cs | 120 +++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs (limited to 'MediaBrowser.Model') diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 74f1a8965..fff7136b8 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -171,6 +171,7 @@ - [tallbl0nde](https://github.com/tallbl0nde) - [sleepycatcoding](https://github.com/sleepycatcoding) - [scampower3](https://github.com/scampower3) + - [Chris-Codes-It] (https://github.com/Chris-Codes-It) # Emby Contributors @@ -241,4 +242,4 @@ - [Jakob Kukla](https://github.com/jakobkukla) - [Utku Özdemir](https://github.com/utkuozdemir) - [JPUC1143](https://github.com/Jpuc1143/) - - [0x25CBFC4F](https://github.com/0x25CBFC4F) + - [0x25CBFC4F](https://github.com/0x25CBFC4F) \ No newline at end of file diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index f5aff07db..219ed5d5f 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Xml.Serialization; using Jellyfin.Data.Enums; using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule; @@ -174,8 +175,10 @@ namespace MediaBrowser.Model.Users public int RemoteClientBitrateLimit { get; set; } [XmlElement(ElementName = "AuthenticationProviderId")] + [Required(AllowEmptyStrings = false)] public string AuthenticationProviderId { get; set; } + [Required(AllowEmptyStrings= false)] public string PasswordResetProviderId { get; set; } /// diff --git a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs new file mode 100644 index 000000000..3f965d0ff --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using AutoFixture.Xunit2; +using Jellyfin.Api.Controllers; +using Jellyfin.Data.Entities; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Users; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using Nikse.SubtitleEdit.Core.Common; +using Xunit; + +namespace Jellyfin.Api.Tests.Controllers; + +public class UserControllerTests +{ + private readonly UserController _subject; + private readonly Mock _mockUserManager; + private readonly Mock _mockSessionManager; + private readonly Mock _mockNetworkManager; + private readonly Mock _mockDeviceManager; + private readonly Mock _mockAuthorizationContext; + private readonly Mock _mockServerConfigurationManager; + private readonly Mock> _mockLogger; + private readonly Mock _mockQuickConnect; + private readonly Mock _mockPlaylistManager; + + public UserControllerTests() + { + _mockUserManager = new Mock(); + _mockSessionManager = new Mock(); + _mockNetworkManager = new Mock(); + _mockDeviceManager = new Mock(); + _mockAuthorizationContext = new Mock(); + _mockServerConfigurationManager = new Mock(); + _mockLogger = new Mock>(); + _mockQuickConnect = new Mock(); + _mockPlaylistManager = new Mock(); + + _subject = new UserController( + _mockUserManager.Object, + _mockSessionManager.Object, + _mockNetworkManager.Object, + _mockDeviceManager.Object, + _mockAuthorizationContext.Object, + _mockServerConfigurationManager.Object, + _mockLogger.Object, + _mockQuickConnect.Object, + _mockPlaylistManager.Object); + } + + [Theory] + [AutoData] + public async Task UpdateUserPolicy_WhenUserNotFound_ReturnsNotFound(Guid userId, UserPolicy userPolicy) + { + User? nullUser = null; + _mockUserManager. + Setup(m => m.GetUserById(userId)) + .Returns(nullUser); + + Assert.IsType(await _subject.UpdateUserPolicy(userId, userPolicy)); + } + + [Theory] + [InlineAutoData(null)] + [InlineAutoData("")] + [InlineAutoData(" ")] + public void UpdateUserPolicy_WhenPasswordResetProviderIdNotSupplied_ReturnsBadRequest(string? passwordResetProviderId) + { + var userPolicy = new UserPolicy + { + PasswordResetProviderId = passwordResetProviderId, + AuthenticationProviderId = "AuthenticationProviderId" + }; + + Assert.Contains( + Validate(userPolicy), v => + v.MemberNames.Contains("PasswordResetProviderId") && + v.ErrorMessage != null && + v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase)); + } + + [Theory] + [InlineAutoData(null)] + [InlineAutoData("")] + [InlineAutoData(" ")] + public void UpdateUserPolicy_WhenAuthenticationProviderIdNotSupplied_ReturnsBadRequest(string? authenticationProviderId) + { + var userPolicy = new UserPolicy + { + AuthenticationProviderId = authenticationProviderId, + PasswordResetProviderId = "PasswordResetProviderId" + }; + + Assert.Contains(Validate(userPolicy), v => + v.MemberNames.Contains("AuthenticationProviderId") && + v.ErrorMessage != null && + v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase)); + } + + private IList Validate(object model) + { + var result = new List(); + var context = new ValidationContext(model, null, null); + Validator.TryValidateObject(model, context, result, true); + + return result; + } +} -- cgit v1.2.3