From 26d7fc828075dbaa3068ac9c323ebef3370fd023 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 20 May 2021 22:10:19 +0200 Subject: Enable nullable reference types for MediaBrowser.MediaEncoding.Subtitles --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 62c0c0bb1..cdb778bf2 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; -- cgit v1.2.3 From adf406b1821800bbf1b214c4ac673defa362e4f4 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 10 Jun 2021 21:07:28 +0300 Subject: Fix playback of complex BluRay ISOs (#6166) --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 3 ++- MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index cdb778bf2..3af618af8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -367,7 +367,8 @@ namespace MediaBrowser.MediaEncoding.Encoder public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) { var prefix = "file"; - if (mediaSource.VideoType == VideoType.BluRay) + if (mediaSource.VideoType == VideoType.BluRay + || mediaSource.IsoType == IsoType.BluRay) { prefix = "bluray"; } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 36b56f20f..12e1fbea5 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -147,7 +147,8 @@ namespace MediaBrowser.Providers.MediaInfo { Path = path, Protocol = protocol, - VideoType = item.VideoType + VideoType = item.VideoType, + IsoType = item.IsoType } }, cancellationToken); -- cgit v1.2.3 From 6f8ccab788e85e025eaa44b67a1487bf419afb53 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 19 Jun 2021 18:02:33 +0200 Subject: Move non-jellyfin extensions to separate project --- Emby.Dlna/DlnaManager.cs | 2 +- Emby.Naming/Audio/AudioFileParser.cs | 2 +- Emby.Naming/Video/VideoResolver.cs | 2 +- .../Channels/ChannelManager.cs | 2 +- .../Data/SqliteItemRepository.cs | 3 +- .../HttpServer/Security/AuthorizationContext.cs | 2 +- .../HttpServer/WebSocketConnection.cs | 2 +- .../IO/ManagedFileSystem.cs | 2 +- .../Library/LibraryManager.cs | 1 + .../Library/LiveStreamHelper.cs | 2 +- .../Library/MediaSourceManager.cs | 2 +- .../Library/Resolvers/Movies/MovieResolver.cs | 2 +- .../LiveTv/EmbyTV/EncodedRecorder.cs | 4 +- .../LiveTv/EmbyTV/ItemDataProvider.cs | 2 +- .../LiveTv/Listings/SchedulesDirect.cs | 4 +- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 3 +- .../LiveTv/TunerHosts/M3uParser.cs | 1 + .../Localization/LocalizationManager.cs | 4 +- .../Plugins/PluginManager.cs | 4 +- .../ScheduledTasks/ScheduledTaskWorker.cs | 2 +- .../Session/SessionManager.cs | 1 + .../Sorting/StudioComparer.cs | 3 +- .../Updates/InstallationManager.cs | 2 +- Jellyfin.Api/BaseJellyfinApiController.cs | 2 +- .../Controllers/ConfigurationController.cs | 2 +- Jellyfin.Api/Controllers/PluginsController.cs | 2 +- Jellyfin.Api/Controllers/TvShowsController.cs | 2 +- Jellyfin.Api/Controllers/UserLibraryController.cs | 2 +- Jellyfin.Api/Extensions/DtoExtensions.cs | 2 +- Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs | 2 +- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 2 +- .../Models/SessionDtos/ClientCapabilitiesDto.cs | 4 +- .../Extensions/ApiServiceCollectionExtensions.cs | 2 +- .../Formatters/CamelCaseJsonProfileFormatter.cs | 2 +- .../Formatters/PascalCaseJsonProfileFormatter.cs | 2 +- .../Middleware/UrlDecodeQueryFeature.cs | 2 +- .../Migrations/Routines/MigrateUserDb.cs | 2 +- Jellyfin.sln | 16 +++ MediaBrowser.Common/Extensions/CopyToExtensions.cs | 26 ---- .../Extensions/EnumerableExtensions.cs | 51 -------- .../Extensions/ShuffleExtensions.cs | 41 ------ .../Extensions/SplitStringExtensions.cs | 95 -------------- MediaBrowser.Common/Extensions/StreamExtensions.cs | 63 --------- .../Extensions/StringBuilderExtensions.cs | 35 ----- .../Json/Converters/JsonBoolNumberConverter.cs | 30 ----- .../Converters/JsonCommaDelimitedArrayConverter.cs | 24 ---- .../JsonCommaDelimitedArrayConverterFactory.cs | 28 ---- .../Json/Converters/JsonDateTimeConverter.cs | 34 ----- .../Json/Converters/JsonDelimitedArrayConverter.cs | 81 ------------ .../Json/Converters/JsonGuidConverter.cs | 26 ---- .../Json/Converters/JsonNullableGuidConverter.cs | 33 ----- .../Json/Converters/JsonNullableStructConverter.cs | 45 ------- .../JsonNullableStructConverterFactory.cs | 27 ---- .../JsonOmdbNotAvailableInt32Converter.cs | 44 ------- .../JsonOmdbNotAvailableStringConverter.cs | 41 ------ .../Converters/JsonPipeDelimitedArrayConverter.cs | 24 ---- .../JsonPipeDelimitedArrayConverterFactory.cs | 28 ---- .../Json/Converters/JsonStringConverter.cs | 39 ------ .../Json/Converters/JsonVersionConverter.cs | 23 ---- MediaBrowser.Common/Json/JsonDefaults.cs | 90 ------------- .../BaseItemManager/BaseItemManager.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 1 + .../Entities/CollectionFolder.cs | 2 +- MediaBrowser.Controller/Entities/Extensions.cs | 2 +- .../Extensions/StringExtensions.cs | 21 --- .../Sorting/AlphanumComparator.cs | 137 -------------------- MediaBrowser.Controller/Sorting/SortExtensions.cs | 5 +- .../Images/EpisodeLocalImageProvider.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- .../Subtitles/SubtitleEditParser.cs | 2 +- .../Entities/JsonLowerCaseConverter.cs | 29 ----- MediaBrowser.Model/Entities/VirtualFolderInfo.cs | 1 + MediaBrowser.Model/MediaBrowser.Model.csproj | 3 +- .../Plugins/AudioDb/AlbumImageProvider.cs | 2 +- .../Plugins/AudioDb/AlbumProvider.cs | 2 +- .../Plugins/AudioDb/ArtistImageProvider.cs | 2 +- .../Plugins/AudioDb/ArtistProvider.cs | 2 +- .../Omdb/JsonOmdbNotAvailableInt32Converter.cs | 46 +++++++ .../Omdb/JsonOmdbNotAvailableStringConverter.cs | 43 ++++++ .../Plugins/Omdb/OmdbItemProvider.cs | 4 +- .../Plugins/Omdb/OmdbProvider.cs | 4 +- MediaBrowser.Providers/Properties/AssemblyInfo.cs | 2 +- .../Studios/StudiosImageProvider.cs | 2 +- src/Jellyfin.Extensions/AlphanumericComparator.cs | 144 +++++++++++++++++++++ src/Jellyfin.Extensions/CopyToExtensions.cs | 26 ++++ src/Jellyfin.Extensions/EnumerableExtensions.cs | 51 ++++++++ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 30 +++++ .../Json/Converters/JsonBoolNumberConverter.cs | 30 +++++ .../Converters/JsonCommaDelimitedArrayConverter.cs | 24 ++++ .../JsonCommaDelimitedArrayConverterFactory.cs | 28 ++++ .../Json/Converters/JsonDateTimeConverter.cs | 34 +++++ .../Json/Converters/JsonDelimitedArrayConverter.cs | 81 ++++++++++++ .../Json/Converters/JsonGuidConverter.cs | 26 ++++ .../Json/Converters/JsonLowerCaseConverter.cs | 25 ++++ .../Json/Converters/JsonNullableGuidConverter.cs | 33 +++++ .../Json/Converters/JsonNullableStructConverter.cs | 45 +++++++ .../JsonNullableStructConverterFactory.cs | 27 ++++ .../Converters/JsonPipeDelimitedArrayConverter.cs | 24 ++++ .../JsonPipeDelimitedArrayConverterFactory.cs | 28 ++++ .../Json/Converters/JsonStringConverter.cs | 39 ++++++ .../Json/Converters/JsonVersionConverter.cs | 23 ++++ src/Jellyfin.Extensions/Json/JsonDefaults.cs | 90 +++++++++++++ src/Jellyfin.Extensions/ShuffleExtensions.cs | 41 ++++++ src/Jellyfin.Extensions/SplitStringExtensions.cs | 115 ++++++++++++++++ src/Jellyfin.Extensions/StreamExtensions.cs | 63 +++++++++ src/Jellyfin.Extensions/StringBuilderExtensions.cs | 35 +++++ src/Jellyfin.Extensions/StringExtensions.cs | 31 +++++ .../Extensions/CopyToExtensionsTests.cs | 40 ------ .../Extensions/ShuffleExtensionsTests.cs | 22 ---- .../Json/JsonBoolNumberTests.cs | 45 ------- .../Json/JsonCommaDelimitedArrayTests.cs | 142 -------------------- .../Json/JsonCommaDelimitedIReadOnlyListTests.cs | 92 ------------- .../Json/JsonGuidConverterTests.cs | 68 ---------- .../Json/JsonNullableGuidConverterTests.cs | 80 ------------ .../Json/JsonOmdbConverterTests.cs | 87 ------------- .../Json/JsonStringConverterTests.cs | 38 ------ .../Json/JsonVersionConverterTests.cs | 36 ------ .../Models/GenericBodyArrayModel.cs | 20 --- .../Models/GenericBodyIReadOnlyListModel.cs | 19 --- .../AlphanumComparatorTests.cs | 30 ----- .../Extensions/StringExtensionsTests.cs | 19 --- .../AlphanumericComparatorTests.cs | 29 +++++ .../CopyToExtensionsTests.cs | 39 ++++++ .../Jellyfin.Extensions.Tests.csproj | 38 ++++++ .../Json/Converters/JsonBoolNumberTests.cs | 45 +++++++ .../Converters/JsonCommaDelimitedArrayTests.cs | 142 ++++++++++++++++++++ .../JsonCommaDelimitedIReadOnlyListTests.cs | 92 +++++++++++++ .../Json/Converters/JsonGuidConverterTests.cs | 68 ++++++++++ .../Json/Converters/JsonLowerCaseConverterTests.cs | 71 ++++++++++ .../Converters/JsonNullableGuidConverterTests.cs | 80 ++++++++++++ .../Json/Converters/JsonStringConverterTests.cs | 38 ++++++ .../Json/Converters/JsonVersionConverterTests.cs | 36 ++++++ .../Json/Models/GenericBodyArrayModel.cs | 20 +++ .../Json/Models/GenericBodyIReadOnlyListModel.cs | 19 +++ .../ShuffleExtensionsTests.cs | 21 +++ .../StringExtensionsTests.cs | 18 +++ .../FFprobeParserTests.cs | 2 +- .../Probing/ProbeResultNormalizerTests.cs | 2 +- .../Entities/JsonLowerCaseConverterTests.cs | 70 ---------- .../Omdb/JsonOmdbConverterTests.cs | 86 ++++++++++++ .../AuthHelper.cs | 2 +- .../Controllers/DashboardControllerTests.cs | 2 +- .../Controllers/StartupControllerTests.cs | 2 +- .../Controllers/UserControllerTests.cs | 2 +- 144 files changed, 2113 insertions(+), 1988 deletions(-) delete mode 100644 MediaBrowser.Common/Extensions/CopyToExtensions.cs delete mode 100644 MediaBrowser.Common/Extensions/EnumerableExtensions.cs delete mode 100644 MediaBrowser.Common/Extensions/ShuffleExtensions.cs delete mode 100644 MediaBrowser.Common/Extensions/SplitStringExtensions.cs delete mode 100644 MediaBrowser.Common/Extensions/StreamExtensions.cs delete mode 100644 MediaBrowser.Common/Extensions/StringBuilderExtensions.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonStringConverter.cs delete mode 100644 MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs delete mode 100644 MediaBrowser.Common/Json/JsonDefaults.cs delete mode 100644 MediaBrowser.Controller/Sorting/AlphanumComparator.cs delete mode 100644 MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs create mode 100644 MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs create mode 100644 MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs create mode 100644 src/Jellyfin.Extensions/AlphanumericComparator.cs create mode 100644 src/Jellyfin.Extensions/CopyToExtensions.cs create mode 100644 src/Jellyfin.Extensions/EnumerableExtensions.cs create mode 100644 src/Jellyfin.Extensions/Jellyfin.Extensions.csproj create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonBoolNumberConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonDateTimeConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverterFactory.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonVersionConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/JsonDefaults.cs create mode 100644 src/Jellyfin.Extensions/ShuffleExtensions.cs create mode 100644 src/Jellyfin.Extensions/SplitStringExtensions.cs create mode 100644 src/Jellyfin.Extensions/StreamExtensions.cs create mode 100644 src/Jellyfin.Extensions/StringBuilderExtensions.cs create mode 100644 src/Jellyfin.Extensions/StringExtensions.cs delete mode 100644 tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Extensions/ShuffleExtensionsTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs delete mode 100644 tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs delete mode 100644 tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs delete mode 100644 tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs delete mode 100644 tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonGuidConverterTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonNullableGuidConverterTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonVersionConverterTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs create mode 100644 tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs delete mode 100644 tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs create mode 100644 tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index a1b106704..b08f7590d 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -14,9 +14,9 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Emby.Dlna.Profiles; using Emby.Dlna.Server; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs index af4aa0059..2b610ec79 100644 --- a/Emby.Naming/Audio/AudioFileParser.cs +++ b/Emby.Naming/Audio/AudioFileParser.cs @@ -1,7 +1,7 @@ using System; using System.IO; using Emby.Naming.Common; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; namespace Emby.Naming.Audio { diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index c4ac5fdc6..3b1d906c6 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -2,7 +2,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; using Emby.Naming.Common; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; namespace Emby.Naming.Video { diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 448f12403..093607dd5 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 9b147b5d7..35aa589a1 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -13,8 +13,9 @@ using System.Text.Json; using System.Threading; using Emby.Server.Implementations.Playlists; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index c87f7dbbd..488614609 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Net; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 8f7d60669..5d38ea0ca 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -7,7 +7,7 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Net; using MediaBrowser.Model.Session; diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 64d802457..ca028a3ca 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -6,8 +6,8 @@ using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 028673529..d80637332 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -21,6 +21,7 @@ using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index 4ef7923db..806269182 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -12,7 +12,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index b812b6b61..91c9e61cf 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -15,7 +15,7 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 97f96f746..889e29a6b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -6,7 +6,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Video; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 26e4ef1ed..93781cb7b 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -11,9 +11,9 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index bdab8c3e4..4a031e475 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.EmbyTV diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 00d02873c..b7639a51c 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -15,7 +15,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Cryptography; @@ -789,7 +789,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var channelNumber = GetChannelNumber(channel); - var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase)) + var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase)) ?? new ScheduleDirect.Station { stationID = channel.stationID diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 54de841fe..011748d1d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -12,8 +12,9 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 40a162890..c9657f605 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -10,6 +10,7 @@ using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index b1ff28c2c..a9e3bfdb0 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -8,8 +8,8 @@ using System.IO; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions; +using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 8fd61f2bc..fc0920edf 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -10,8 +10,8 @@ using System.Text.Json; using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; -using MediaBrowser.Common.Json.Converters; +using Jellyfin.Extensions.Json; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Configuration; diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index d7e320754..b34325041 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Progress; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 62df354fd..c4b19f417 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Extensions; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs index 01445c525..6826aee3b 100644 --- a/Emby.Server.Implementations/Sorting/StudioComparer.cs +++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs @@ -4,6 +4,7 @@ using System; using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -30,7 +31,7 @@ namespace Emby.Server.Implementations.Sorting throw new ArgumentNullException(nameof(y)); } - return AlphanumComparator.CompareValues(x.Studios.FirstOrDefault() ?? string.Empty, y.Studios.FirstOrDefault() ?? string.Empty); + return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault()); } /// diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index b0921cbd8..7b0afa4e2 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -11,7 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index 1c1fc71d7..59d6b7513 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,5 +1,5 @@ using System.Net.Mime; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index b6309baab..60529e990 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 7a6130719..0ae6109bc 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -8,8 +8,8 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.PluginDtos; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Json; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index ffb726fab..51d40994e 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -6,7 +6,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 1d70406ac..a33a0826c 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -9,7 +9,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index 06173315a..5e338b67d 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using Jellyfin.Api.Helpers; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 8913180e4..411e4c550 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Json.Converters; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index 65d4b644e..0761b2085 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -using MediaBrowser.Common.Json.Converters; +using Jellyfin.Extensions.Json.Converters; namespace Jellyfin.Api.Models.PlaylistDtos { diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs index e58095536..fa62472e1 100644 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -using MediaBrowser.Common.Json.Converters; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Session; @@ -85,4 +85,4 @@ namespace Jellyfin.Api.Models.SessionDtos }; } } -} \ No newline at end of file +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 924b250ce..15dc43856 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -21,11 +21,11 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Extensions.Json; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; using Jellyfin.Server.Formatters; -using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index c349e3dca..ea8c5ecdb 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs index 0480f5e0e..03ca7dda7 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -1,5 +1,5 @@ using System.Net.Mime; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs index 310a3d31a..c1f5b5dfa 100644 --- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs +++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Web; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 96bd2ccc4..d9524645a 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -4,9 +4,9 @@ using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions.Json; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Users; -using MediaBrowser.Common.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; diff --git a/Jellyfin.sln b/Jellyfin.sln index 9fbd9d266..4626601c3 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -83,6 +83,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Integration EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Providers.Tests", "tests\Jellyfin.Providers.Tests\Jellyfin.Providers.Tests.csproj", "{A964008C-2136-4716-B6CB-B3426C22320A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Extensions", "src\Jellyfin.Extensions\Jellyfin.Extensions.csproj", "{750B8757-BE3D-4F8C-941A-FBAD94904ADA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Extensions.Tests", "tests\Jellyfin.Extensions.Tests\Jellyfin.Extensions.Tests.csproj", "{332A5C7A-F907-47CA-910E-BE6F7371B9E0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -229,6 +235,14 @@ Global {A964008C-2136-4716-B6CB-B3426C22320A}.Debug|Any CPU.Build.0 = Debug|Any CPU {A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.ActiveCfg = Release|Any CPU {A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.Build.0 = Release|Any CPU + {750B8757-BE3D-4F8C-941A-FBAD94904ADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {750B8757-BE3D-4F8C-941A-FBAD94904ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {750B8757-BE3D-4F8C-941A-FBAD94904ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {750B8757-BE3D-4F8C-941A-FBAD94904ADA}.Release|Any CPU.Build.0 = Release|Any CPU + {332A5C7A-F907-47CA-910E-BE6F7371B9E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {332A5C7A-F907-47CA-910E-BE6F7371B9E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {332A5C7A-F907-47CA-910E-BE6F7371B9E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {332A5C7A-F907-47CA-910E-BE6F7371B9E0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -247,6 +261,8 @@ Global {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {A964008C-2136-4716-B6CB-B3426C22320A} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {750B8757-BE3D-4F8C-941A-FBAD94904ADA} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} + {332A5C7A-F907-47CA-910E-BE6F7371B9E0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/MediaBrowser.Common/Extensions/CopyToExtensions.cs b/MediaBrowser.Common/Extensions/CopyToExtensions.cs deleted file mode 100644 index 2ecbc6539..000000000 --- a/MediaBrowser.Common/Extensions/CopyToExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; - -namespace MediaBrowser.Common.Extensions -{ - /// - /// Provides CopyTo extensions methods for . - /// - public static class CopyToExtensions - { - /// - /// Copies all the elements of the current collection to the specified list - /// starting at the specified destination array index. The index is specified as a 32-bit integer. - /// - /// The current collection that is the source of the elements. - /// The list that is the destination of the elements copied from the current collection. - /// A 32-bit integer that represents the index in destination at which copying begins. - /// The type of the array. - public static void CopyTo(this IReadOnlyList source, IList destination, int index = 0) - { - for (int i = 0; i < source.Count; i++) - { - destination[index + i] = source[i]; - } - } - } -} diff --git a/MediaBrowser.Common/Extensions/EnumerableExtensions.cs b/MediaBrowser.Common/Extensions/EnumerableExtensions.cs deleted file mode 100644 index 2b8a6c395..000000000 --- a/MediaBrowser.Common/Extensions/EnumerableExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MediaBrowser.Common.Extensions -{ - /// - /// Static extensions for the interface. - /// - public static class EnumerableExtensions - { - /// - /// Determines whether the value is contained in the source collection. - /// - /// An instance of the interface. - /// The value to look for in the collection. - /// The string comparison. - /// A value indicating whether the value is contained in the collection. - /// The source is null. - public static bool Contains(this IEnumerable source, ReadOnlySpan value, StringComparison stringComparison) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (source is IList list) - { - int len = list.Count; - for (int i = 0; i < len; i++) - { - if (value.Equals(list[i], stringComparison)) - { - return true; - } - } - - return false; - } - - foreach (string element in source) - { - if (value.Equals(element, stringComparison)) - { - return true; - } - } - - return false; - } - } -} diff --git a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs deleted file mode 100644 index 2604abf85..000000000 --- a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MediaBrowser.Common.Extensions -{ - /// - /// Provides Shuffle extensions methods for . - /// - public static class ShuffleExtensions - { - private static readonly Random _rng = new Random(); - - /// - /// Shuffles the items in a list. - /// - /// The list that should get shuffled. - /// The type. - public static void Shuffle(this IList list) - { - list.Shuffle(_rng); - } - - /// - /// Shuffles the items in a list. - /// - /// The list that should get shuffled. - /// The random number generator to use. - /// The type. - public static void Shuffle(this IList list, Random rng) - { - int n = list.Count; - while (n > 1) - { - int k = rng.Next(n--); - T value = list[k]; - list[k] = list[n]; - list[n] = value; - } - } - } -} diff --git a/MediaBrowser.Common/Extensions/SplitStringExtensions.cs b/MediaBrowser.Common/Extensions/SplitStringExtensions.cs deleted file mode 100644 index 9c9108495..000000000 --- a/MediaBrowser.Common/Extensions/SplitStringExtensions.cs +++ /dev/null @@ -1,95 +0,0 @@ -/* -MIT License - -Copyright (c) 2019 Gérald Barré - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - */ - -#pragma warning disable CS1591 -#pragma warning disable CA1034 -using System; -using System.Diagnostics.Contracts; -using System.Runtime.InteropServices; - -namespace MediaBrowser.Common.Extensions -{ - /// - /// Extension class for splitting lines without unnecessary allocations. - /// - public static class SplitStringExtensions - { - /// - /// Creates a new string split enumerator. - /// - /// The string to split. - /// The separator to split on. - /// The enumerator struct. - [Pure] - public static SplitEnumerator SpanSplit(this string str, char separator) => new (str.AsSpan(), separator); - - /// - /// Creates a new span split enumerator. - /// - /// The span to split. - /// The separator to split on. - /// The enumerator struct. - [Pure] - public static SplitEnumerator Split(this ReadOnlySpan str, char separator) => new (str, separator); - - [StructLayout(LayoutKind.Auto)] - public ref struct SplitEnumerator - { - private readonly char _separator; - private ReadOnlySpan _str; - - public SplitEnumerator(ReadOnlySpan str, char separator) - { - _str = str; - _separator = separator; - Current = default; - } - - public ReadOnlySpan Current { get; private set; } - - public readonly SplitEnumerator GetEnumerator() => this; - - public bool MoveNext() - { - if (_str.Length == 0) - { - return false; - } - - var span = _str; - var index = span.IndexOf(_separator); - if (index == -1) - { - _str = ReadOnlySpan.Empty; - Current = span; - return true; - } - - Current = span.Slice(0, index); - _str = span[(index + 1)..]; - return true; - } - } - } -} diff --git a/MediaBrowser.Common/Extensions/StreamExtensions.cs b/MediaBrowser.Common/Extensions/StreamExtensions.cs deleted file mode 100644 index 5cbf57d98..000000000 --- a/MediaBrowser.Common/Extensions/StreamExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace MediaBrowser.Common.Extensions -{ - /// - /// Class BaseExtensions. - /// - public static class StreamExtensions - { - /// - /// Reads all lines in the . - /// - /// The to read from. - /// All lines in the stream. - public static string[] ReadAllLines(this Stream stream) - => ReadAllLines(stream, Encoding.UTF8); - - /// - /// Reads all lines in the . - /// - /// The to read from. - /// The character encoding to use. - /// All lines in the stream. - public static string[] ReadAllLines(this Stream stream, Encoding encoding) - { - using (StreamReader reader = new StreamReader(stream, encoding)) - { - return ReadAllLines(reader).ToArray(); - } - } - - /// - /// Reads all lines in the . - /// - /// The to read from. - /// All lines in the stream. - public static IEnumerable ReadAllLines(this TextReader reader) - { - string? line; - while ((line = reader.ReadLine()) != null) - { - yield return line; - } - } - - /// - /// Reads all lines in the . - /// - /// The to read from. - /// All lines in the stream. - public static async IAsyncEnumerable ReadAllLinesAsync(this TextReader reader) - { - string? line; - while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) - { - yield return line; - } - } - } -} diff --git a/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs deleted file mode 100644 index 75d654f23..000000000 --- a/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Text; - -namespace MediaBrowser.Common.Extensions -{ - /// - /// Extension methods for the class. - /// - public static class StringBuilderExtensions - { - /// - /// Concatenates and appends the members of a collection in single quotes using the specified delimiter. - /// - /// The string builder. - /// The character delimiter. - /// The collection of strings to concatenate. - /// The updated string builder. - public static StringBuilder AppendJoinInSingleQuotes(this StringBuilder builder, char delimiter, IReadOnlyList values) - { - var len = values.Count; - for (var i = 0; i < len; i++) - { - builder.Append('\'') - .Append(values[i]) - .Append('\'') - .Append(delimiter); - } - - // remove last , - builder.Length--; - - return builder; - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs b/MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs deleted file mode 100644 index b29e6a71a..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converts a number to a boolean. - /// This is needed for HDHomerun. - /// - public class JsonBoolNumberConverter : JsonConverter - { - /// - public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Number) - { - return Convert.ToBoolean(reader.GetInt32()); - } - - return reader.GetBoolean(); - } - - /// - public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) - { - writer.WriteBooleanValue(value); - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs deleted file mode 100644 index 127a41a06..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.ComponentModel; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Convert comma delimited string to array of type. - /// - /// Type to convert to. - public sealed class JsonCommaDelimitedArrayConverter : JsonDelimitedArrayConverter - { - /// - /// Initializes a new instance of the class. - /// - public JsonCommaDelimitedArrayConverter() : base() - { - } - - /// - protected override char Delimiter => ','; - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs deleted file mode 100644 index de41348dd..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Json comma delimited array converter factory. - /// - /// - /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. - /// - public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory - { - /// - public override bool CanConvert(Type typeToConvert) - { - return true; - } - - /// - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; - return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType)); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs deleted file mode 100644 index 73e3a0493..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Legacy DateTime converter. - /// Milliseconds aren't output if zero by default. - /// - public class JsonDateTimeConverter : JsonConverter - { - /// - public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.GetDateTime(); - } - - /// - public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) - { - if (value.Millisecond == 0) - { - // Remaining ticks value will be 0, manually format. - writer.WriteStringValue(value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffZ", CultureInfo.InvariantCulture)); - } - else - { - writer.WriteStringValue(value); - } - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs deleted file mode 100644 index b691798c9..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.ComponentModel; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Convert delimited string to array of type. - /// - /// Type to convert to. - public abstract class JsonDelimitedArrayConverter : JsonConverter - { - private readonly TypeConverter _typeConverter; - - /// - /// Initializes a new instance of the class. - /// - protected JsonDelimitedArrayConverter() - { - _typeConverter = TypeDescriptor.GetConverter(typeof(T)); - } - - /// - /// Gets the array delimiter. - /// - protected virtual char Delimiter { get; } - - /// - public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - // GetString can't return null here because we already handled it above - var stringEntries = reader.GetString()?.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); - if (stringEntries == null || stringEntries.Length == 0) - { - return Array.Empty(); - } - - var parsedValues = new object[stringEntries.Length]; - var convertedCount = 0; - for (var i = 0; i < stringEntries.Length; i++) - { - try - { - parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim()); - convertedCount++; - } - catch (FormatException) - { - // TODO log when upgraded to .Net6 - // https://github.com/dotnet/runtime/issues/42975 - // _logger.LogDebug(e, "Error converting value."); - } - } - - var typedValues = new T[convertedCount]; - var typedValueIndex = 0; - for (var i = 0; i < stringEntries.Length; i++) - { - if (parsedValues[i] != null) - { - typedValues.SetValue(parsedValues[i], typedValueIndex); - typedValueIndex++; - } - } - - return typedValues; - } - - return JsonSerializer.Deserialize(ref reader, options); - } - - /// - public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs b/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs deleted file mode 100644 index bd9600110..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converts a GUID object or value to/from JSON. - /// - public class JsonGuidConverter : JsonConverter - { - /// - public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var guidStr = reader.GetString(); - return guidStr == null ? Guid.Empty : new Guid(guidStr); - } - - /// - public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture)); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs deleted file mode 100644 index 6d96d5496..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converts a GUID object or value to/from JSON. - /// - public class JsonNullableGuidConverter : JsonConverter - { - /// - public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var guidStr = reader.GetString(); - return guidStr == null ? null : new Guid(guidStr); - } - - /// - public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options) - { - if (value == null || value == Guid.Empty) - { - writer.WriteNullValue(); - } - else - { - writer.WriteStringValue(value.Value.ToString("N", CultureInfo.InvariantCulture)); - } - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs deleted file mode 100644 index 0501f7b2a..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converts a nullable struct or value to/from JSON. - /// Required - some clients send an empty string. - /// - /// The struct type. - public class JsonNullableStructConverter : JsonConverter - where TStruct : struct - { - /// - public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - - // Token is empty string. - if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty)) - { - return null; - } - - return JsonSerializer.Deserialize(ref reader, options); - } - - /// - public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options) - { - if (value.HasValue) - { - JsonSerializer.Serialize(writer, value.Value, options); - } - else - { - writer.WriteNullValue(); - } - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs deleted file mode 100644 index e2a3d798a..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Json nullable struct converter factory. - /// - public class JsonNullableStructConverterFactory : JsonConverterFactory - { - /// - public override bool CanConvert(Type typeToConvert) - { - return typeToConvert.IsGenericType - && typeToConvert.GetGenericTypeDefinition() == typeof(Nullable<>) - && typeToConvert.GenericTypeArguments[0].IsValueType; - } - - /// - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - var structType = typeToConvert.GenericTypeArguments[0]; - return (JsonConverter?)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType)); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs deleted file mode 100644 index 3d97a9de5..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.ComponentModel; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converts a string N/A to string.Empty. - /// - public class JsonOmdbNotAvailableInt32Converter : JsonConverter - { - /// - public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - var str = reader.GetString(); - if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var converter = TypeDescriptor.GetConverter(typeToConvert); - return (int?)converter.ConvertFromString(str); - } - - return JsonSerializer.Deserialize(ref reader, options); - } - - /// - public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) - { - if (value.HasValue) - { - writer.WriteNumberValue(value.Value); - } - else - { - writer.WriteNullValue(); - } - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs deleted file mode 100644 index 77cf46b70..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converts a string N/A to string.Empty. - /// - public class JsonOmdbNotAvailableStringConverter : JsonConverter - { - /// - public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - - if (reader.TokenType == JsonTokenType.String) - { - // GetString can't return null here because we already handled it above - var str = reader.GetString()!; - if (str.Equals("N/A", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - return str; - } - - return JsonSerializer.Deserialize(ref reader, options); - } - - /// - public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) - { - writer.WriteStringValue(value); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs deleted file mode 100644 index a8f6cfbec..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.ComponentModel; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Convert Pipe delimited string to array of type. - /// - /// Type to convert to. - public sealed class JsonPipeDelimitedArrayConverter : JsonDelimitedArrayConverter - { - /// - /// Initializes a new instance of the class. - /// - public JsonPipeDelimitedArrayConverter() : base() - { - } - - /// - protected override char Delimiter => '|'; - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs deleted file mode 100644 index 1bebc49ec..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Json Pipe delimited array converter factory. - /// - /// - /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. - /// - public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory - { - /// - public override bool CanConvert(Type typeToConvert) - { - return true; - } - - /// - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; - return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType)); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs deleted file mode 100644 index 6cd980e48..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Buffers; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converter to allow the serializer to read strings. - /// - public class JsonStringConverter : JsonConverter - { - /// - public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.TokenType switch - { - JsonTokenType.Null => null, - JsonTokenType.String => reader.GetString(), - _ => GetRawValue(reader) - }; - } - - /// - public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) - { - writer.WriteStringValue(value); - } - - private static string GetRawValue(Utf8JsonReader reader) - { - var utf8Bytes = reader.HasValueSequence - ? reader.ValueSequence.ToArray() - : reader.ValueSpan; - return Encoding.UTF8.GetString(utf8Bytes); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs b/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs deleted file mode 100644 index 81c093c54..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converts a Version object or value to/from JSON. - /// - /// - /// Required to send as a string instead of an object. - /// - public class JsonVersionConverter : JsonConverter - { - /// - public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => new Version(reader.GetString()!); // Will throw ArgumentNullException on null - - /// - public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString()); - } -} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs deleted file mode 100644 index 405d6125f..000000000 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using MediaBrowser.Common.Json.Converters; - -namespace MediaBrowser.Common.Json -{ - /// - /// Helper class for having compatible JSON throughout the codebase. - /// - public static class JsonDefaults - { - /// - /// Pascal case json profile media type. - /// - public const string PascalCaseMediaType = "application/json; profile=\"PascalCase\""; - - /// - /// Camel case json profile media type. - /// - public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\""; - - /// - /// When changing these options, update - /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs - /// -> AddJellyfinApi - /// -> AddJsonOptions. - /// - private static readonly JsonSerializerOptions _jsonSerializerOptions = new () - { - ReadCommentHandling = JsonCommentHandling.Disallow, - WriteIndented = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = - { - new JsonGuidConverter(), - new JsonNullableGuidConverter(), - new JsonVersionConverter(), - new JsonStringEnumConverter(), - new JsonNullableStructConverterFactory(), - new JsonBoolNumberConverter(), - new JsonDateTimeConverter(), - new JsonStringConverter() - } - }; - - private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new (_jsonSerializerOptions) - { - PropertyNamingPolicy = null - }; - - private static readonly JsonSerializerOptions _camelCaseJsonSerializerOptions = new (_jsonSerializerOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - /// - /// Gets the default options. - /// - /// - /// The return value must not be modified. - /// If the defaults must be modified the author must use the copy constructor. - /// - /// The default options. - public static JsonSerializerOptions Options - => _jsonSerializerOptions; - - /// - /// Gets camelCase json options. - /// - /// - /// The return value must not be modified. - /// If the defaults must be modified the author must use the copy constructor. - /// - /// The camelCase options. - public static JsonSerializerOptions CamelCaseOptions - => _camelCaseJsonSerializerOptions; - - /// - /// Gets PascalCase json options. - /// - /// - /// The return value must not be modified. - /// If the defaults must be modified the author must use the copy constructor. - /// - /// The PascalCase options. - public static JsonSerializerOptions PascalCaseOptions - => _pascalCaseJsonSerializerOptions; - } -} diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index ffc274c5d..97f40b537 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -3,7 +3,7 @@ using System; using System.Linq; using System.Threading; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 6137ddbf7..a6c22c93d 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -13,6 +13,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 4a721ca44..4f367fe2b 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -10,7 +10,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs index 244cc00be..d8bc0069c 100644 --- a/MediaBrowser.Controller/Entities/Extensions.cs +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -2,7 +2,7 @@ using System; using System.Linq; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs index f1af01345..48bd9522a 100644 --- a/MediaBrowser.Controller/Extensions/StringExtensions.cs +++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs @@ -21,27 +21,6 @@ namespace MediaBrowser.Controller.Extensions return Normalize(string.Concat(chars), NormalizationForm.FormC); } - /// - /// Counts the number of occurrences of [needle] in the string. - /// - /// The haystack to search in. - /// The character to search for. - /// The number of occurrences of the [needle] character. - public static int Count(this ReadOnlySpan value, char needle) - { - var count = 0; - var length = value.Length; - for (var i = 0; i < length; i++) - { - if (value[i] == needle) - { - count++; - } - } - - return count; - } - private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true) { if (stripStringOnFailure) diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs deleted file mode 100644 index e00cadca2..000000000 --- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs +++ /dev/null @@ -1,137 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; - -namespace MediaBrowser.Controller.Sorting -{ - public class AlphanumComparator : IComparer - { - public static int CompareValues(string? s1, string? s2) - { - if (s1 == null && s2 == null) - { - return 0; - } - else if (s1 == null) - { - return -1; - } - else if (s2 == null) - { - return 1; - } - - int len1 = s1.Length; - int len2 = s2.Length; - - // Early return for empty strings - if (len1 == 0 && len2 == 0) - { - return 0; - } - else if (len1 == 0) - { - return -1; - } - else if (len2 == 0) - { - return 1; - } - - int pos1 = 0; - int pos2 = 0; - - do - { - int start1 = pos1; - int start2 = pos2; - - bool isNum1 = char.IsDigit(s1[pos1++]); - bool isNum2 = char.IsDigit(s2[pos2++]); - - while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1) - { - pos1++; - } - - while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2) - { - pos2++; - } - - var span1 = s1.AsSpan(start1, pos1 - start1); - var span2 = s2.AsSpan(start2, pos2 - start2); - - if (isNum1 && isNum2) - { - // Trim leading zeros so we can compare the length - // of the strings to find the largest number - span1 = span1.TrimStart('0'); - span2 = span2.TrimStart('0'); - var span1Len = span1.Length; - var span2Len = span2.Length; - if (span1Len < span2Len) - { - return -1; - } - else if (span1Len > span2Len) - { - return 1; - } - else if (span1Len >= 20) // Number is probably too big for a ulong - { - // Trim all the first digits that are the same - int i = 0; - while (i < span1Len && span1[i] == span2[i]) - { - i++; - } - - // If there are no more digits it's the same number - if (i == span1Len) - { - continue; - } - - // Only need to compare the most significant digit - span1 = span1.Slice(i, 1); - span2 = span2.Slice(i, 1); - } - - if (!ulong.TryParse(span1, out var num1) - || !ulong.TryParse(span2, out var num2)) - { - return 0; - } - else if (num1 < num2) - { - return -1; - } - else if (num1 > num2) - { - return 1; - } - } - else - { - int result = span1.CompareTo(span2, StringComparison.InvariantCulture); - if (result != 0) - { - return result; - } - } -#pragma warning disable SA1500 // TODO remove with StyleCop.Analyzers v1.2.0 https://github.com/DotNetAnalyzers/StyleCopAnalyzers/pull/3196 - } while (pos1 < len1 && pos2 < len2); -#pragma warning restore SA1500 - - return len1 - len2; - } - - /// - public int Compare(string? x, string? y) - { - return CompareValues(x, y); - } - } -} diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index aa6ec513f..f9c0d39dd 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -1,16 +1,15 @@ -#nullable disable - #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Extensions; namespace MediaBrowser.Controller.Sorting { public static class SortExtensions { - private static readonly AlphanumComparator _comparer = new AlphanumComparator(); + private static readonly AlphanumericComparator _comparer = new AlphanumericComparator(); public static IEnumerable OrderByString(this IEnumerable list, Func getName) { diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs index fdba64c4a..dc13bf4f6 100644 --- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 3af618af8..412a95321 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -11,9 +11,9 @@ using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs index 639a34d99..24ceb1b57 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs @@ -2,7 +2,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; using Nikse.SubtitleEdit.Core; diff --git a/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs b/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs deleted file mode 100644 index 7c627f0e3..000000000 --- a/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs +++ /dev/null @@ -1,29 +0,0 @@ -#nullable disable -// THIS IS A HACK -// TODO: @bond Move to separate project - -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Model.Entities -{ - /// - /// Converts an object to a lowercase string. - /// - /// The object type. - public class JsonLowerCaseConverter : JsonConverter - { - /// - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return JsonSerializer.Deserialize(ref reader, options); - } - - /// - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value?.ToString().ToLowerInvariant()); - } - } -} diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs index 8fed392b9..2b2bda12c 100644 --- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs +++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs @@ -3,6 +3,7 @@ using System; using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Model.Configuration; namespace MediaBrowser.Model.Entities diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 4db99f0b0..c475d905a 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -50,7 +50,8 @@ - + + diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs index 85a28747f..36d8eeb40 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs @@ -6,7 +6,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs index 25bb3f9ce..9539c396d 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs @@ -11,7 +11,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs index db8536cc9..aa61a56f6 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs @@ -6,7 +6,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs index cbb61fa35..b2f05d76d 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs @@ -10,7 +10,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; diff --git a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs new file mode 100644 index 000000000..268538815 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs @@ -0,0 +1,46 @@ +#nullable enable + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Providers.Plugins.Omdb +{ + /// + /// Converts a string N/A to string.Empty. + /// + public class JsonOmdbNotAvailableInt32Converter : JsonConverter + { + /// + public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var str = reader.GetString(); + if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var converter = TypeDescriptor.GetConverter(typeToConvert); + return (int?)converter.ConvertFromString(str); + } + + return JsonSerializer.Deserialize(ref reader, options); + } + + /// + public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + writer.WriteNumberValue(value.Value); + } + else + { + writer.WriteNullValue(); + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs new file mode 100644 index 000000000..c19589d45 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs @@ -0,0 +1,43 @@ +#nullable enable + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Providers.Plugins.Omdb +{ + /// + /// Converts a string N/A to string.Empty. + /// + public class JsonOmdbNotAvailableStringConverter : JsonConverter + { + /// + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType == JsonTokenType.String) + { + // GetString can't return null here because we already handled it above + var str = reader.GetString()!; + if (str.Equals("N/A", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return str; + } + + return JsonSerializer.Deserialize(ref reader, options); + } + + /// + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 428b0ded1..78eea02e0 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -10,8 +10,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; -using MediaBrowser.Common.Json; -using MediaBrowser.Common.Json.Converters; +using Jellyfin.Extensions.Json; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 46d303890..5d9fd36d3 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -10,8 +10,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; -using MediaBrowser.Common.Json; -using MediaBrowser.Common.Json.Converters; +using Jellyfin.Extensions.Json; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Providers/Properties/AssemblyInfo.cs b/MediaBrowser.Providers/Properties/AssemblyInfo.cs index fe4749c79..bd301b5f0 100644 --- a/MediaBrowser.Providers/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Providers/Properties/AssemblyInfo.cs @@ -15,7 +15,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] -[assembly: InternalsVisibleTo("Jellyfin.Common.Tests")] +[assembly: InternalsVisibleTo("Jellyfin.Providers.Tests")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index f6153dd53..63e78d15e 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/src/Jellyfin.Extensions/AlphanumericComparator.cs b/src/Jellyfin.Extensions/AlphanumericComparator.cs new file mode 100644 index 000000000..e3c81eba8 --- /dev/null +++ b/src/Jellyfin.Extensions/AlphanumericComparator.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Extensions +{ + /// + /// Alphanumeric . + /// + public class AlphanumericComparator : IComparer + { + /// + /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other. + /// + /// The first object to compare. + /// The second object to compare. + /// A signed integer that indicates the relative values of x and y. + public static int CompareValues(string? s1, string? s2) + { + if (s1 == null && s2 == null) + { + return 0; + } + else if (s1 == null) + { + return -1; + } + else if (s2 == null) + { + return 1; + } + + int len1 = s1.Length; + int len2 = s2.Length; + + // Early return for empty strings + if (len1 == 0 && len2 == 0) + { + return 0; + } + else if (len1 == 0) + { + return -1; + } + else if (len2 == 0) + { + return 1; + } + + int pos1 = 0; + int pos2 = 0; + + do + { + int start1 = pos1; + int start2 = pos2; + + bool isNum1 = char.IsDigit(s1[pos1++]); + bool isNum2 = char.IsDigit(s2[pos2++]); + + while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1) + { + pos1++; + } + + while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2) + { + pos2++; + } + + var span1 = s1.AsSpan(start1, pos1 - start1); + var span2 = s2.AsSpan(start2, pos2 - start2); + + if (isNum1 && isNum2) + { + // Trim leading zeros so we can compare the length + // of the strings to find the largest number + span1 = span1.TrimStart('0'); + span2 = span2.TrimStart('0'); + var span1Len = span1.Length; + var span2Len = span2.Length; + if (span1Len < span2Len) + { + return -1; + } + else if (span1Len > span2Len) + { + return 1; + } + else if (span1Len >= 20) // Number is probably too big for a ulong + { + // Trim all the first digits that are the same + int i = 0; + while (i < span1Len && span1[i] == span2[i]) + { + i++; + } + + // If there are no more digits it's the same number + if (i == span1Len) + { + continue; + } + + // Only need to compare the most significant digit + span1 = span1.Slice(i, 1); + span2 = span2.Slice(i, 1); + } + + if (!ulong.TryParse(span1, out var num1) + || !ulong.TryParse(span2, out var num2)) + { + return 0; + } + else if (num1 < num2) + { + return -1; + } + else if (num1 > num2) + { + return 1; + } + } + else + { + int result = span1.CompareTo(span2, StringComparison.InvariantCulture); + if (result != 0) + { + return result; + } + } +#pragma warning disable SA1500 // TODO remove with StyleCop.Analyzers v1.2.0 https://github.com/DotNetAnalyzers/StyleCopAnalyzers/pull/3196 + } while (pos1 < len1 && pos2 < len2); +#pragma warning restore SA1500 + + return len1 - len2; + } + + /// + public int Compare(string? x, string? y) + { + return CompareValues(x, y); + } + } +} diff --git a/src/Jellyfin.Extensions/CopyToExtensions.cs b/src/Jellyfin.Extensions/CopyToExtensions.cs new file mode 100644 index 000000000..72d37b5b6 --- /dev/null +++ b/src/Jellyfin.Extensions/CopyToExtensions.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Jellyfin.Extensions +{ + /// + /// Provides CopyTo extensions methods for . + /// + public static class CopyToExtensions + { + /// + /// Copies all the elements of the current collection to the specified list + /// starting at the specified destination array index. The index is specified as a 32-bit integer. + /// + /// The current collection that is the source of the elements. + /// The list that is the destination of the elements copied from the current collection. + /// A 32-bit integer that represents the index in destination at which copying begins. + /// The type of the array. + public static void CopyTo(this IReadOnlyList source, IList destination, int index = 0) + { + for (int i = 0; i < source.Count; i++) + { + destination[index + i] = source[i]; + } + } + } +} diff --git a/src/Jellyfin.Extensions/EnumerableExtensions.cs b/src/Jellyfin.Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..b5fe93357 --- /dev/null +++ b/src/Jellyfin.Extensions/EnumerableExtensions.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Extensions +{ + /// + /// Static extensions for the interface. + /// + public static class EnumerableExtensions + { + /// + /// Determines whether the value is contained in the source collection. + /// + /// An instance of the interface. + /// The value to look for in the collection. + /// The string comparison. + /// A value indicating whether the value is contained in the collection. + /// The source is null. + public static bool Contains(this IEnumerable source, ReadOnlySpan value, StringComparison stringComparison) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source is IList list) + { + int len = list.Count; + for (int i = 0; i < len; i++) + { + if (value.Equals(list[i], stringComparison)) + { + return true; + } + } + + return false; + } + + foreach (string element in source) + { + if (value.Equals(element, stringComparison)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj new file mode 100644 index 000000000..c7b9a4ad0 --- /dev/null +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -0,0 +1,30 @@ + + + + net5.0 + false + true + true + enable + AllEnabledByDefault + ../../jellyfin.ruleset + + + + Jellyfin Contributors + https://github.com/jellyfin/jellyfin + GPL-3.0-only + + + + + + + + + + + + + + diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonBoolNumberConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonBoolNumberConverter.cs new file mode 100644 index 000000000..c2543cf7c --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonBoolNumberConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Converts a number to a boolean. + /// This is needed for HDHomerun. + /// + public class JsonBoolNumberConverter : JsonConverter + { + /// + public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Number) + { + return Convert.ToBoolean(reader.GetInt32()); + } + + return reader.GetBoolean(); + } + + /// + public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) + { + writer.WriteBooleanValue(value); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs new file mode 100644 index 000000000..44980ec02 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Convert comma delimited string to array of type. + /// + /// Type to convert to. + public sealed class JsonCommaDelimitedArrayConverter : JsonDelimitedArrayConverter + { + /// + /// Initializes a new instance of the class. + /// + public JsonCommaDelimitedArrayConverter() : base() + { + } + + /// + protected override char Delimiter => ','; + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs new file mode 100644 index 000000000..cc9311a24 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Json comma delimited array converter factory. + /// + /// + /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. + /// + public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) + { + return true; + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; + return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType)); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDateTimeConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDateTimeConverter.cs new file mode 100644 index 000000000..8ae080b15 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDateTimeConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Legacy DateTime converter. + /// Milliseconds aren't output if zero by default. + /// + public class JsonDateTimeConverter : JsonConverter + { + /// + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetDateTime(); + } + + /// + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + if (value.Millisecond == 0) + { + // Remaining ticks value will be 0, manually format. + writer.WriteStringValue(value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffZ", CultureInfo.InvariantCulture)); + } + else + { + writer.WriteStringValue(value); + } + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs new file mode 100644 index 000000000..c39805aa3 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -0,0 +1,81 @@ +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Convert delimited string to array of type. + /// + /// Type to convert to. + public abstract class JsonDelimitedArrayConverter : JsonConverter + { + private readonly TypeConverter _typeConverter; + + /// + /// Initializes a new instance of the class. + /// + protected JsonDelimitedArrayConverter() + { + _typeConverter = TypeDescriptor.GetConverter(typeof(T)); + } + + /// + /// Gets the array delimiter. + /// + protected virtual char Delimiter { get; } + + /// + public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + // GetString can't return null here because we already handled it above + var stringEntries = reader.GetString()?.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); + if (stringEntries == null || stringEntries.Length == 0) + { + return Array.Empty(); + } + + var parsedValues = new object[stringEntries.Length]; + var convertedCount = 0; + for (var i = 0; i < stringEntries.Length; i++) + { + try + { + parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim()); + convertedCount++; + } + catch (FormatException) + { + // TODO log when upgraded to .Net6 + // https://github.com/dotnet/runtime/issues/42975 + // _logger.LogDebug(e, "Error converting value."); + } + } + + var typedValues = new T[convertedCount]; + var typedValueIndex = 0; + for (var i = 0; i < stringEntries.Length; i++) + { + if (parsedValues[i] != null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; + } + + return JsonSerializer.Deserialize(ref reader, options); + } + + /// + public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs new file mode 100644 index 000000000..be94dd519 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Converts a GUID object or value to/from JSON. + /// + public class JsonGuidConverter : JsonConverter + { + /// + public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var guidStr = reader.GetString(); + return guidStr == null ? Guid.Empty : new Guid(guidStr); + } + + /// + public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture)); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs new file mode 100644 index 000000000..cd582ced6 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Converts an object to a lowercase string. + /// + /// The object type. + public class JsonLowerCaseConverter : JsonConverter + { + /// + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize(ref reader, options); + } + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString()?.ToLowerInvariant()); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs new file mode 100644 index 000000000..6192d1598 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Converts a GUID object or value to/from JSON. + /// + public class JsonNullableGuidConverter : JsonConverter + { + /// + public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var guidStr = reader.GetString(); + return guidStr == null ? null : new Guid(guidStr); + } + + /// + public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options) + { + if (value == null || value == Guid.Empty) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStringValue(value.Value.ToString("N", CultureInfo.InvariantCulture)); + } + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs new file mode 100644 index 000000000..6de238b39 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Converts a nullable struct or value to/from JSON. + /// Required - some clients send an empty string. + /// + /// The struct type. + public class JsonNullableStructConverter : JsonConverter + where TStruct : struct + { + /// + public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + // Token is empty string. + if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty)) + { + return null; + } + + return JsonSerializer.Deserialize(ref reader, options); + } + + /// + public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options) + { + if (value.HasValue) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + else + { + writer.WriteNullValue(); + } + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverterFactory.cs new file mode 100644 index 000000000..e7749589a --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverterFactory.cs @@ -0,0 +1,27 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Json nullable struct converter factory. + /// + public class JsonNullableStructConverterFactory : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsGenericType + && typeToConvert.GetGenericTypeDefinition() == typeof(Nullable<>) + && typeToConvert.GenericTypeArguments[0].IsValueType; + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var structType = typeToConvert.GenericTypeArguments[0]; + return (JsonConverter?)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType)); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs new file mode 100644 index 000000000..e3e492e24 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Convert Pipe delimited string to array of type. + /// + /// Type to convert to. + public sealed class JsonPipeDelimitedArrayConverter : JsonDelimitedArrayConverter + { + /// + /// Initializes a new instance of the class. + /// + public JsonPipeDelimitedArrayConverter() : base() + { + } + + /// + protected override char Delimiter => '|'; + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs new file mode 100644 index 000000000..579674f18 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Json Pipe delimited array converter factory. + /// + /// + /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. + /// + public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) + { + return true; + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; + return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType)); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs new file mode 100644 index 000000000..1a7a8c4f5 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Buffers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Converter to allow the serializer to read strings. + /// + public class JsonStringConverter : JsonConverter + { + /// + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Null => null, + JsonTokenType.String => reader.GetString(), + _ => GetRawValue(reader) + }; + } + + /// + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + + private static string GetRawValue(Utf8JsonReader reader) + { + var utf8Bytes = reader.HasValueSequence + ? reader.ValueSequence.ToArray() + : reader.ValueSpan; + return Encoding.UTF8.GetString(utf8Bytes); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonVersionConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonVersionConverter.cs new file mode 100644 index 000000000..51ffec1cb --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonVersionConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Converts a Version object or value to/from JSON. + /// + /// + /// Required to send as a string instead of an object. + /// + public class JsonVersionConverter : JsonConverter + { + /// + public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => new Version(reader.GetString()!); // Will throw ArgumentNullException on null + + /// + public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString()); + } +} diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs new file mode 100644 index 000000000..f4ec91123 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs @@ -0,0 +1,90 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json.Converters; + +namespace Jellyfin.Extensions.Json +{ + /// + /// Helper class for having compatible JSON throughout the codebase. + /// + public static class JsonDefaults + { + /// + /// Pascal case json profile media type. + /// + public const string PascalCaseMediaType = "application/json; profile=\"PascalCase\""; + + /// + /// Camel case json profile media type. + /// + public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\""; + + /// + /// When changing these options, update + /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs + /// -> AddJellyfinApi + /// -> AddJsonOptions. + /// + private static readonly JsonSerializerOptions _jsonSerializerOptions = new () + { + ReadCommentHandling = JsonCommentHandling.Disallow, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + Converters = + { + new JsonGuidConverter(), + new JsonNullableGuidConverter(), + new JsonVersionConverter(), + new JsonStringEnumConverter(), + new JsonNullableStructConverterFactory(), + new JsonBoolNumberConverter(), + new JsonDateTimeConverter(), + new JsonStringConverter() + } + }; + + private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new (_jsonSerializerOptions) + { + PropertyNamingPolicy = null + }; + + private static readonly JsonSerializerOptions _camelCaseJsonSerializerOptions = new (_jsonSerializerOptions) + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// + /// Gets the default options. + /// + /// + /// The return value must not be modified. + /// If the defaults must be modified the author must use the copy constructor. + /// + /// The default options. + public static JsonSerializerOptions Options + => _jsonSerializerOptions; + + /// + /// Gets camelCase json options. + /// + /// + /// The return value must not be modified. + /// If the defaults must be modified the author must use the copy constructor. + /// + /// The camelCase options. + public static JsonSerializerOptions CamelCaseOptions + => _camelCaseJsonSerializerOptions; + + /// + /// Gets PascalCase json options. + /// + /// + /// The return value must not be modified. + /// If the defaults must be modified the author must use the copy constructor. + /// + /// The PascalCase options. + public static JsonSerializerOptions PascalCaseOptions + => _pascalCaseJsonSerializerOptions; + } +} diff --git a/src/Jellyfin.Extensions/ShuffleExtensions.cs b/src/Jellyfin.Extensions/ShuffleExtensions.cs new file mode 100644 index 000000000..4e481983f --- /dev/null +++ b/src/Jellyfin.Extensions/ShuffleExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Extensions +{ + /// + /// Provides Shuffle extensions methods for . + /// + public static class ShuffleExtensions + { + private static readonly Random _rng = new Random(); + + /// + /// Shuffles the items in a list. + /// + /// The list that should get shuffled. + /// The type. + public static void Shuffle(this IList list) + { + list.Shuffle(_rng); + } + + /// + /// Shuffles the items in a list. + /// + /// The list that should get shuffled. + /// The random number generator to use. + /// The type. + public static void Shuffle(this IList list, Random rng) + { + int n = list.Count; + while (n > 1) + { + int k = rng.Next(n--); + T value = list[k]; + list[k] = list[n]; + list[n] = value; + } + } + } +} diff --git a/src/Jellyfin.Extensions/SplitStringExtensions.cs b/src/Jellyfin.Extensions/SplitStringExtensions.cs new file mode 100644 index 000000000..5fa5c0123 --- /dev/null +++ b/src/Jellyfin.Extensions/SplitStringExtensions.cs @@ -0,0 +1,115 @@ +/* +MIT License + +Copyright (c) 2019 Gérald Barré + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +// TODO: remove when analyzer is fixed: https://github.com/dotnet/roslyn-analyzers/issues/5158 +#pragma warning disable CA1034 // Nested types should not be visible + +using System; +using System.Diagnostics.Contracts; +using System.Runtime.InteropServices; + +namespace Jellyfin.Extensions +{ + /// + /// Extension class for splitting lines without unnecessary allocations. + /// + public static class SplitStringExtensions + { + /// + /// Creates a new string split enumerator. + /// + /// The string to split. + /// The separator to split on. + /// The enumerator struct. + [Pure] + public static Enumerator SpanSplit(this string str, char separator) => new (str.AsSpan(), separator); + + /// + /// Creates a new span split enumerator. + /// + /// The span to split. + /// The separator to split on. + /// The enumerator struct. + [Pure] + public static Enumerator Split(this ReadOnlySpan str, char separator) => new (str, separator); + + /// + /// Provides an enumerator for the substrings seperated by the separator. + /// + [StructLayout(LayoutKind.Auto)] + public ref struct Enumerator + { + private readonly char _separator; + private ReadOnlySpan _str; + + /// + /// Initializes a new instance of the struct. + /// + /// The span to split. + /// The separator to split on. + public Enumerator(ReadOnlySpan str, char separator) + { + _str = str; + _separator = separator; + Current = default; + } + + /// + /// Gets a reference to the item at the current position of the enumerator. + /// + public ReadOnlySpan Current { get; private set; } + + /// + /// Returns this. + /// + /// this. + public readonly Enumerator GetEnumerator() => this; + + /// + /// Advances the enumerator to the next item. + /// + /// true if there is a next element; otherwise false. + public bool MoveNext() + { + if (_str.Length == 0) + { + return false; + } + + var span = _str; + var index = span.IndexOf(_separator); + if (index == -1) + { + _str = ReadOnlySpan.Empty; + Current = span; + return true; + } + + Current = span.Slice(0, index); + _str = span[(index + 1)..]; + return true; + } + } + } +} diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs new file mode 100644 index 000000000..9751d9d42 --- /dev/null +++ b/src/Jellyfin.Extensions/StreamExtensions.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Jellyfin.Extensions +{ + /// + /// Class BaseExtensions. + /// + public static class StreamExtensions + { + /// + /// Reads all lines in the . + /// + /// The to read from. + /// All lines in the stream. + public static string[] ReadAllLines(this Stream stream) + => ReadAllLines(stream, Encoding.UTF8); + + /// + /// Reads all lines in the . + /// + /// The to read from. + /// The character encoding to use. + /// All lines in the stream. + public static string[] ReadAllLines(this Stream stream, Encoding encoding) + { + using (StreamReader reader = new StreamReader(stream, encoding)) + { + return ReadAllLines(reader).ToArray(); + } + } + + /// + /// Reads all lines in the . + /// + /// The to read from. + /// All lines in the stream. + public static IEnumerable ReadAllLines(this TextReader reader) + { + string? line; + while ((line = reader.ReadLine()) != null) + { + yield return line; + } + } + + /// + /// Reads all lines in the . + /// + /// The to read from. + /// All lines in the stream. + public static async IAsyncEnumerable ReadAllLinesAsync(this TextReader reader) + { + string? line; + while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) + { + yield return line; + } + } + } +} diff --git a/src/Jellyfin.Extensions/StringBuilderExtensions.cs b/src/Jellyfin.Extensions/StringBuilderExtensions.cs new file mode 100644 index 000000000..02ff7cc1f --- /dev/null +++ b/src/Jellyfin.Extensions/StringBuilderExtensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Text; + +namespace Jellyfin.Extensions +{ + /// + /// Extension methods for the class. + /// + public static class StringBuilderExtensions + { + /// + /// Concatenates and appends the members of a collection in single quotes using the specified delimiter. + /// + /// The string builder. + /// The character delimiter. + /// The collection of strings to concatenate. + /// The updated string builder. + public static StringBuilder AppendJoinInSingleQuotes(this StringBuilder builder, char delimiter, IReadOnlyList values) + { + var len = values.Count; + for (var i = 0; i < len; i++) + { + builder.Append('\'') + .Append(values[i]) + .Append('\'') + .Append(delimiter); + } + + // remove last , + builder.Length--; + + return builder; + } + } +} diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs new file mode 100644 index 000000000..acc695ed2 --- /dev/null +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -0,0 +1,31 @@ +using System; + +namespace Jellyfin.Extensions +{ + /// + /// Provides extensions methods for . + /// + public static class StringExtensions + { + /// + /// Counts the number of occurrences of [needle] in the string. + /// + /// The haystack to search in. + /// The character to search for. + /// The number of occurrences of the [needle] character. + public static int Count(this ReadOnlySpan value, char needle) + { + var count = 0; + var length = value.Length; + for (var i = 0; i < length; i++) + { + if (value[i] == needle) + { + count++; + } + } + + return count; + } + } +} diff --git a/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs b/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs deleted file mode 100644 index 9903409fa..000000000 --- a/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Common.Extensions; -using Xunit; - -namespace Jellyfin.Common.Tests.Extensions -{ - public static class CopyToExtensionsTests - { - public static IEnumerable CopyTo_Valid_Correct_TestData() - { - yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } }; - yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } }; - } - - [Theory] - [MemberData(nameof(CopyTo_Valid_Correct_TestData))] - public static void CopyTo_Valid_Correct(IReadOnlyList source, IList destination, int index, IList expected) - { - source.CopyTo(destination, index); - Assert.Equal(expected, destination); - } - - public static IEnumerable CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData() - { - yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 }; - yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 }; - yield return new object[] { new[] { 0, 1, 2 }, Array.Empty(), 0 }; - yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 }; - yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 }; - } - - [Theory] - [MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))] - public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException(IReadOnlyList source, IList destination, int index) - { - Assert.Throws(() => source.CopyTo(destination, index)); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Extensions/ShuffleExtensionsTests.cs b/tests/Jellyfin.Common.Tests/Extensions/ShuffleExtensionsTests.cs deleted file mode 100644 index cbdbcf112..000000000 --- a/tests/Jellyfin.Common.Tests/Extensions/ShuffleExtensionsTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using MediaBrowser.Common.Extensions; -using Xunit; - -namespace Jellyfin.Common.Tests.Extensions -{ - public static class ShuffleExtensionsTests - { - private static readonly Random _rng = new Random(); - - [Fact] - public static void Shuffle_Valid_Correct() - { - byte[] original = new byte[1 << 6]; - _rng.NextBytes(original); - byte[] shuffled = (byte[])original.Clone(); - shuffled.Shuffle(); - - Assert.NotEqual(original, shuffled); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs deleted file mode 100644 index 7629d9912..000000000 --- a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using FsCheck; -using FsCheck.Xunit; -using MediaBrowser.Common.Json.Converters; -using Xunit; - -namespace Jellyfin.Common.Tests.Json -{ - public class JsonBoolNumberTests - { - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() - { - Converters = - { - new JsonBoolNumberConverter() - } - }; - - [Theory] - [InlineData("1", true)] - [InlineData("0", false)] - [InlineData("2", true)] - [InlineData("true", true)] - [InlineData("false", false)] - public void Deserialize_Number_Valid_Success(string input, bool? output) - { - var value = JsonSerializer.Deserialize(input, _jsonOptions); - Assert.Equal(value, output); - } - - [Theory] - [InlineData(true, "true")] - [InlineData(false, "false")] - public void Serialize_Bool_Success(bool input, string output) - { - var value = JsonSerializer.Serialize(input, _jsonOptions); - Assert.Equal(value, output); - } - - [Property] - public Property Deserialize_NonZeroInt_True(NonZeroInt input) - => JsonSerializer.Deserialize(input.ToString(), _jsonOptions).ToProperty(); - } -} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs deleted file mode 100644 index ca300401d..000000000 --- a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using Jellyfin.Common.Tests.Models; -using MediaBrowser.Model.Session; -using Xunit; - -namespace Jellyfin.Common.Tests.Json -{ - public static class JsonCommaDelimitedArrayTests - { - [Fact] - public static void Deserialize_String_Null_Success() - { - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": null }", options); - Assert.Null(value?.Value); - } - - [Fact] - public static void Deserialize_Empty_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = Array.Empty() - }; - - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": """" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_String_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = new[] { "a", "b", "c" } - }; - - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_String_Space_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = new[] { "a", "b", "c" } - }; - - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_GenericCommandType_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } - }; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_GenericCommandType_EmptyEntry_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } - }; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_GenericCommandType_Invalid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } - }; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_GenericCommandType_Space_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } - }; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_String_Array_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = new[] { "a", "b", "c" } - }; - - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_GenericCommandType_Array_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } - }; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs deleted file mode 100644 index 34ad9bac7..000000000 --- a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Jellyfin.Common.Tests.Models; -using MediaBrowser.Model.Session; -using Xunit; - -namespace Jellyfin.Common.Tests.Json -{ - public static class JsonCommaDelimitedIReadOnlyListTests - { - [Fact] - public static void Deserialize_String_Valid_Success() - { - var desiredValue = new GenericBodyIReadOnlyListModel - { - Value = new[] { "a", "b", "c" } - }; - - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_String_Space_Valid_Success() - { - var desiredValue = new GenericBodyIReadOnlyListModel - { - Value = new[] { "a", "b", "c" } - }; - - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_GenericCommandType_Valid_Success() - { - var desiredValue = new GenericBodyIReadOnlyListModel - { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } - }; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_GenericCommandType_Space_Valid_Success() - { - var desiredValue = new GenericBodyIReadOnlyListModel - { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } - }; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_String_Array_Valid_Success() - { - var desiredValue = new GenericBodyIReadOnlyListModel - { - Value = new[] { "a", "b", "c" } - }; - - var options = new JsonSerializerOptions(); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public static void Deserialize_GenericCommandType_Array_Valid_Success() - { - var desiredValue = new GenericBodyIReadOnlyListModel - { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } - }; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonStringEnumConverter()); - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options); - Assert.Equal(desiredValue.Value, value?.Value); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs deleted file mode 100644 index dbfad3c2f..000000000 --- a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Text.Json; -using MediaBrowser.Common.Json.Converters; -using Xunit; - -namespace Jellyfin.Common.Tests.Json -{ - public class JsonGuidConverterTests - { - private readonly JsonSerializerOptions _options; - - public JsonGuidConverterTests() - { - _options = new JsonSerializerOptions(); - _options.Converters.Add(new JsonGuidConverter()); - } - - [Fact] - public void Deserialize_Valid_Success() - { - Guid value = JsonSerializer.Deserialize(@"""a852a27afe324084ae66db579ee3ee18""", _options); - Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value); - } - - [Fact] - public void Deserialize_ValidDashed_Success() - { - Guid value = JsonSerializer.Deserialize(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", _options); - Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value); - } - - [Fact] - public void Roundtrip_Valid_Success() - { - Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18"); - string value = JsonSerializer.Serialize(guid, _options); - Assert.Equal(guid, JsonSerializer.Deserialize(value, _options)); - } - - [Fact] - public void Deserialize_Null_EmptyGuid() - { - Assert.Equal(Guid.Empty, JsonSerializer.Deserialize("null", _options)); - } - - [Fact] - public void Serialize_EmptyGuid_EmptyGuid() - { - Assert.Equal($"\"{Guid.Empty:N}\"", JsonSerializer.Serialize(Guid.Empty, _options)); - } - - [Fact] - public void Serialize_Valid_NoDash_Success() - { - var guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA"); - var str = JsonSerializer.Serialize(guid, _options); - Assert.Equal($"\"{guid:N}\"", str); - } - - [Fact] - public void Serialize_Nullable_Success() - { - Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA"); - var str = JsonSerializer.Serialize(guid, _options); - Assert.Equal($"\"{guid:N}\"", str); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs deleted file mode 100644 index cb3b66c4c..000000000 --- a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Text.Json; -using MediaBrowser.Common.Json.Converters; -using Xunit; - -namespace Jellyfin.Common.Tests.Json -{ - public class JsonNullableGuidConverterTests - { - private readonly JsonSerializerOptions _options; - - public JsonNullableGuidConverterTests() - { - _options = new JsonSerializerOptions(); - _options.Converters.Add(new JsonNullableGuidConverter()); - } - - [Fact] - public void Deserialize_Valid_Success() - { - Guid? value = JsonSerializer.Deserialize(@"""a852a27afe324084ae66db579ee3ee18""", _options); - Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value); - } - - [Fact] - public void Deserialize_ValidDashed_Success() - { - Guid? value = JsonSerializer.Deserialize(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", _options); - Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value); - } - - [Fact] - public void Roundtrip_Valid_Success() - { - Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18"); - string value = JsonSerializer.Serialize(guid, _options); - Assert.Equal(guid, JsonSerializer.Deserialize(value, _options)); - } - - [Fact] - public void Deserialize_Null_Null() - { - Assert.Null(JsonSerializer.Deserialize("null", _options)); - } - - [Fact] - public void Deserialize_EmptyGuid_EmptyGuid() - { - Assert.Equal(Guid.Empty, JsonSerializer.Deserialize(@"""00000000-0000-0000-0000-000000000000""", _options)); - } - - [Fact] - public void Serialize_EmptyGuid_Null() - { - Assert.Equal("null", JsonSerializer.Serialize((Guid?)Guid.Empty, _options)); - } - - [Fact] - public void Serialize_Null_Null() - { - Assert.Equal("null", JsonSerializer.Serialize((Guid?)null, _options)); - } - - [Fact] - public void Serialize_Valid_NoDash_Success() - { - var guid = (Guid?)new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA"); - var str = JsonSerializer.Serialize(guid, _options); - Assert.Equal($"\"{guid:N}\"", str); - } - - [Fact] - public void Serialize_Nullable_Success() - { - Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA"); - var str = JsonSerializer.Serialize(guid, _options); - Assert.Equal($"\"{guid:N}\"", str); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs deleted file mode 100644 index efe8063a0..000000000 --- a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using MediaBrowser.Common.Json.Converters; -using MediaBrowser.Providers.Plugins.Omdb; -using Xunit; - -namespace Jellyfin.Common.Tests.Json -{ - public class JsonOmdbConverterTests - { - private readonly JsonSerializerOptions _options; - - public JsonOmdbConverterTests() - { - _options = new JsonSerializerOptions(); - _options.Converters.Add(new JsonOmdbNotAvailableStringConverter()); - _options.Converters.Add(new JsonOmdbNotAvailableInt32Converter()); - _options.NumberHandling = JsonNumberHandling.AllowReadingFromString; - } - - [Fact] - public void Deserialize_Omdb_Response_Not_Available_Success() - { - const string Input = "{\"Title\":\"Chapter 1\",\"Year\":\"2013\",\"Rated\":\"TV-MA\",\"Released\":\"01 Feb 2013\",\"Season\":\"N/A\",\"Episode\":\"N/A\",\"Runtime\":\"55 min\",\"Genre\":\"Drama\",\"Director\":\"David Fincher\",\"Writer\":\"Michael Dobbs (based on the novels by), Andrew Davies (based on the mini-series by), Beau Willimon (created for television by), Beau Willimon, Sam Forman (staff writer)\",\"Actors\":\"Kevin Spacey, Robin Wright, Kate Mara, Corey Stoll\",\"Plot\":\"Congressman Francis Underwood has been declined the chair for Secretary of State. He's now gathering his own team to plot his revenge. Zoe Barnes, a reporter for the Washington Herald, will do anything to get her big break.\",\"Language\":\"English\",\"Country\":\"USA\",\"Awards\":\"N/A\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMTY5MTU4NDQzNV5BMl5BanBnXkFtZTgwMzk2ODcxMzE@._V1_SX300.jpg\",\"Ratings\":[{\"Source\":\"Internet Movie Database\",\"Value\":\"8.7/10\"}],\"Metascore\":\"N/A\",\"imdbRating\":\"8.7\",\"imdbVotes\":\"6736\",\"imdbID\":\"tt2161930\",\"seriesID\":\"N/A\",\"Type\":\"episode\",\"Response\":\"True\"}"; - var seasonRootObject = JsonSerializer.Deserialize(Input, _options); - Assert.NotNull(seasonRootObject); - Assert.Null(seasonRootObject?.Awards); - Assert.Null(seasonRootObject?.Episode); - Assert.Null(seasonRootObject?.Metascore); - } - - [Theory] - [InlineData("\"N/A\"")] - [InlineData("null")] - public void Deserialization_To_Nullable_Int_Shoud_Be_Null(string input) - { - var result = JsonSerializer.Deserialize(input, _options); - Assert.Null(result); - } - - [Theory] - [InlineData("\"8\"", 8)] - [InlineData("8", 8)] - public void Deserialize_NullableInt_Success(string input, int? expected) - { - var result = JsonSerializer.Deserialize(input, _options); - Assert.Equal(result, expected); - } - - [Theory] - [InlineData("\"N/A\"")] - [InlineData("null")] - public void Deserialization_To_Nullable_String_Shoud_Be_Null(string input) - { - var result = JsonSerializer.Deserialize(input, _options); - Assert.Null(result); - } - - [Theory] - [InlineData("\"Jellyfin\"", "Jellyfin")] - public void Deserialize_Normal_String_Success(string input, string expected) - { - var result = JsonSerializer.Deserialize(input, _options); - Assert.Equal(expected, result); - } - - [Fact] - public void Roundtrip_Valid_Success() - { - const string Input = "{\"Title\":\"Chapter 1\",\"Year\":\"2013\",\"Rated\":\"TV-MA\",\"Released\":\"01 Feb 2013\",\"Season\":\"N/A\",\"Episode\":\"N/A\",\"Runtime\":\"55 min\",\"Genre\":\"Drama\",\"Director\":\"David Fincher\",\"Writer\":\"Michael Dobbs (based on the novels by), Andrew Davies (based on the mini-series by), Beau Willimon (created for television by), Beau Willimon, Sam Forman (staff writer)\",\"Actors\":\"Kevin Spacey, Robin Wright, Kate Mara, Corey Stoll\",\"Plot\":\"Congressman Francis Underwood has been declined the chair for Secretary of State. He's now gathering his own team to plot his revenge. Zoe Barnes, a reporter for the Washington Herald, will do anything to get her big break.\",\"Language\":\"English\",\"Country\":\"USA\",\"Awards\":\"N/A\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMTY5MTU4NDQzNV5BMl5BanBnXkFtZTgwMzk2ODcxMzE@._V1_SX300.jpg\",\"Ratings\":[{\"Source\":\"Internet Movie Database\",\"Value\":\"8.7/10\"}],\"Metascore\":\"N/A\",\"imdbRating\":\"8.7\",\"imdbVotes\":\"6736\",\"imdbID\":\"tt2161930\",\"seriesID\":\"N/A\",\"Type\":\"episode\",\"Response\":\"True\"}"; - var trip1 = JsonSerializer.Deserialize(Input, _options); - Assert.NotNull(trip1); - Assert.NotNull(trip1?.Title); - Assert.Null(trip1?.Awards); - Assert.Null(trip1?.Episode); - Assert.Null(trip1?.Metascore); - - var serializedTrip1 = JsonSerializer.Serialize(trip1!, _options); - var trip2 = JsonSerializer.Deserialize(serializedTrip1, _options); - Assert.NotNull(trip2); - Assert.NotNull(trip2?.Title); - Assert.Null(trip2?.Awards); - Assert.Null(trip2?.Episode); - Assert.Null(trip2?.Metascore); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs deleted file mode 100644 index 2b23c6705..000000000 --- a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text.Json; -using MediaBrowser.Common.Json.Converters; -using Xunit; - -namespace Jellyfin.Common.Tests.Json -{ - public class JsonStringConverterTests - { - private readonly JsonSerializerOptions _jsonSerializerOptions = new () - { - Converters = - { - new JsonStringConverter() - } - }; - - [Theory] - [InlineData("\"test\"", "test")] - [InlineData("123", "123")] - [InlineData("123.45", "123.45")] - [InlineData("true", "true")] - [InlineData("false", "false")] - public void Deserialize_String_Valid_Success(string input, string output) - { - var deserialized = JsonSerializer.Deserialize(input, _jsonSerializerOptions); - Assert.Equal(deserialized, output); - } - - [Fact] - public void Deserialize_Int32asInt32_Valid_Success() - { - const string? input = "123"; - const int output = 123; - var deserialized = JsonSerializer.Deserialize(input, _jsonSerializerOptions); - Assert.Equal(deserialized, output); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs deleted file mode 100644 index f2cefdbf8..000000000 --- a/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Text.Json; -using MediaBrowser.Common.Json.Converters; -using Xunit; - -namespace Jellyfin.Common.Tests.Json -{ - public class JsonVersionConverterTests - { - private readonly JsonSerializerOptions _options; - - public JsonVersionConverterTests() - { - _options = new JsonSerializerOptions(); - _options.Converters.Add(new JsonVersionConverter()); - } - - [Fact] - public void Deserialize_Version_Success() - { - var input = "\"1.025.222\""; - var output = new Version(1, 25, 222); - var deserializedInput = JsonSerializer.Deserialize(input, _options); - Assert.Equal(output, deserializedInput); - } - - [Fact] - public void Serialize_Version_Success() - { - var input = new Version(1, 09, 59); - var output = "\"1.9.59\""; - var serializedInput = JsonSerializer.Serialize(input, _options); - Assert.Equal(output, serializedInput); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs deleted file mode 100644 index 276e1bfbe..000000000 --- a/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; -using MediaBrowser.Common.Json.Converters; - -namespace Jellyfin.Common.Tests.Models -{ - /// - /// The generic body model. - /// - /// The value type. - public class GenericBodyArrayModel - { - /// - /// Gets or sets the value. - /// - [SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")] - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public T[] Value { get; set; } = default!; - } -} diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs b/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs deleted file mode 100644 index 627454b25..000000000 --- a/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; -using MediaBrowser.Common.Json.Converters; - -namespace Jellyfin.Common.Tests.Models -{ - /// - /// The generic body IReadOnlyList model. - /// - /// The value type. - public class GenericBodyIReadOnlyListModel - { - /// - /// Gets or sets the value. - /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList Value { get; set; } = default!; - } -} diff --git a/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs b/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs deleted file mode 100644 index 0adf098c3..000000000 --- a/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Linq; -using MediaBrowser.Controller.Sorting; -using Xunit; - -namespace Jellyfin.Controller.Tests -{ - public class AlphanumComparatorTests - { - // InlineData is pre-sorted - [Theory] - [InlineData(null, "", "1", "9", "10", "a", "z")] - [InlineData("50F", "100F", "SR9", "SR100")] - [InlineData("image-1.jpg", "image-02.jpg", "image-4.jpg", "image-9.jpg", "image-10.jpg", "image-11.jpg", "image-22.jpg")] - [InlineData("Hard drive 2GB", "Hard drive 20GB")] - [InlineData("b", "e", "è", "ě", "f", "g", "k")] - [InlineData("123456789", "123456789a", "abc", "abcd")] - [InlineData("12345678912345678912345678913234567891", "123456789123456789123456789132345678912")] - [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567891")] - [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")] - [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")] - [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")] - public void AlphanumComparatorTest(params string?[] strings) - { - var copy = strings.Reverse().ToArray(); - Array.Sort(copy, new AlphanumComparator()); - Assert.True(strings.SequenceEqual(copy)); - } - } -} diff --git a/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs b/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs deleted file mode 100644 index 576c0a49b..000000000 --- a/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using MediaBrowser.Controller.Extensions; -using Xunit; - -namespace Jellyfin.Controller.Extensions.Tests -{ - public class StringExtensionsTests - { - [Theory] - [InlineData("", '_', 0)] - [InlineData("___", '_', 3)] - [InlineData("test\x00", '\x00', 1)] - [InlineData("Imdb=tt0119567|Tmdb=330|TmdbCollection=328", '|', 2)] - public void ReadOnlySpan_Count_Success(string str, char needle, int count) - { - Assert.Equal(count, str.AsSpan().Count(needle)); - } - } -} diff --git a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs new file mode 100644 index 000000000..7730841a1 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using Xunit; + +namespace Jellyfin.Extensions.Tests +{ + public class AlphanumericComparatorTests + { + // InlineData is pre-sorted + [Theory] + [InlineData(null, "", "1", "9", "10", "a", "z")] + [InlineData("50F", "100F", "SR9", "SR100")] + [InlineData("image-1.jpg", "image-02.jpg", "image-4.jpg", "image-9.jpg", "image-10.jpg", "image-11.jpg", "image-22.jpg")] + [InlineData("Hard drive 2GB", "Hard drive 20GB")] + [InlineData("b", "e", "è", "ě", "f", "g", "k")] + [InlineData("123456789", "123456789a", "abc", "abcd")] + [InlineData("12345678912345678912345678913234567891", "123456789123456789123456789132345678912")] + [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567891")] + [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")] + [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")] + [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")] + public void AlphanumericComparatorTest(params string?[] strings) + { + var copy = strings.Reverse().ToArray(); + Array.Sort(copy, new AlphanumericComparator()); + Assert.True(strings.SequenceEqual(copy)); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs new file mode 100644 index 000000000..6fdca4694 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Jellyfin.Extensions.Tests +{ + public static class CopyToExtensionsTests + { + public static IEnumerable CopyTo_Valid_Correct_TestData() + { + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } }; + yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } }; + } + + [Theory] + [MemberData(nameof(CopyTo_Valid_Correct_TestData))] + public static void CopyTo_Valid_Correct(IReadOnlyList source, IList destination, int index, IList expected) + { + source.CopyTo(destination, index); + Assert.Equal(expected, destination); + } + + public static IEnumerable CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData() + { + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 }; + yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 }; + yield return new object[] { new[] { 0, 1, 2 }, Array.Empty(), 0 }; + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 }; + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 }; + } + + [Theory] + [MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))] + public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException(IReadOnlyList source, IList destination, int index) + { + Assert.Throws(() => source.CopyTo(destination, index)); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj new file mode 100644 index 000000000..4b6dca377 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj @@ -0,0 +1,38 @@ + + + + net5.0 + false + true + enable + AllEnabledByDefault + ../jellyfin-tests.ruleset + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs new file mode 100644 index 000000000..d0e3e9456 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs @@ -0,0 +1,45 @@ +using System.Globalization; +using System.Text.Json; +using FsCheck; +using FsCheck.Xunit; +using Jellyfin.Extensions.Json.Converters; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public class JsonBoolNumberTests + { + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() + { + Converters = + { + new JsonBoolNumberConverter() + } + }; + + [Theory] + [InlineData("1", true)] + [InlineData("0", false)] + [InlineData("2", true)] + [InlineData("true", true)] + [InlineData("false", false)] + public void Deserialize_Number_Valid_Success(string input, bool? output) + { + var value = JsonSerializer.Deserialize(input, _jsonOptions); + Assert.Equal(value, output); + } + + [Theory] + [InlineData(true, "true")] + [InlineData(false, "false")] + public void Serialize_Bool_Success(bool input, string output) + { + var value = JsonSerializer.Serialize(input, _jsonOptions); + Assert.Equal(value, output); + } + + [Property] + public Property Deserialize_NonZeroInt_True(NonZeroInt input) + => JsonSerializer.Deserialize(input.ToString(), _jsonOptions).ToProperty(); + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs new file mode 100644 index 000000000..f2ca2ff08 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Tests.Json.Models; +using MediaBrowser.Model.Session; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public static class JsonCommaDelimitedArrayTests + { + [Fact] + public static void Deserialize_String_Null_Success() + { + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": null }", options); + Assert.Null(value?.Value); + } + + [Fact] + public static void Deserialize_Empty_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = Array.Empty() + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": """" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_String_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = new[] { "a", "b", "c" } + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_String_Space_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = new[] { "a", "b", "c" } + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_EmptyEntry_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Invalid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Space_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_String_Array_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = new[] { "a", "b", "c" } + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Array_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs new file mode 100644 index 000000000..92886dcd2 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs @@ -0,0 +1,92 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Tests.Json.Models; +using MediaBrowser.Model.Session; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public static class JsonCommaDelimitedIReadOnlyListTests + { + [Fact] + public static void Deserialize_String_Valid_Success() + { + var desiredValue = new GenericBodyIReadOnlyListModel + { + Value = new[] { "a", "b", "c" } + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_String_Space_Valid_Success() + { + var desiredValue = new GenericBodyIReadOnlyListModel + { + Value = new[] { "a", "b", "c" } + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Valid_Success() + { + var desiredValue = new GenericBodyIReadOnlyListModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Space_Valid_Success() + { + var desiredValue = new GenericBodyIReadOnlyListModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_String_Array_Valid_Success() + { + var desiredValue = new GenericBodyIReadOnlyListModel + { + Value = new[] { "a", "b", "c" } + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Array_Valid_Success() + { + var desiredValue = new GenericBodyIReadOnlyListModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonGuidConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonGuidConverterTests.cs new file mode 100644 index 000000000..8465d465a --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonGuidConverterTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Text.Json; +using Jellyfin.Extensions.Json.Converters; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public class JsonGuidConverterTests + { + private readonly JsonSerializerOptions _options; + + public JsonGuidConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new JsonGuidConverter()); + } + + [Fact] + public void Deserialize_Valid_Success() + { + Guid value = JsonSerializer.Deserialize(@"""a852a27afe324084ae66db579ee3ee18""", _options); + Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value); + } + + [Fact] + public void Deserialize_ValidDashed_Success() + { + Guid value = JsonSerializer.Deserialize(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", _options); + Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value); + } + + [Fact] + public void Roundtrip_Valid_Success() + { + Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18"); + string value = JsonSerializer.Serialize(guid, _options); + Assert.Equal(guid, JsonSerializer.Deserialize(value, _options)); + } + + [Fact] + public void Deserialize_Null_EmptyGuid() + { + Assert.Equal(Guid.Empty, JsonSerializer.Deserialize("null", _options)); + } + + [Fact] + public void Serialize_EmptyGuid_EmptyGuid() + { + Assert.Equal($"\"{Guid.Empty:N}\"", JsonSerializer.Serialize(Guid.Empty, _options)); + } + + [Fact] + public void Serialize_Valid_NoDash_Success() + { + var guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA"); + var str = JsonSerializer.Serialize(guid, _options); + Assert.Equal($"\"{guid:N}\"", str); + } + + [Fact] + public void Serialize_Nullable_Success() + { + Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA"); + var str = JsonSerializer.Serialize(guid, _options); + Assert.Equal($"\"{guid:N}\"", str); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs new file mode 100644 index 000000000..af9227de2 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json.Converters; +using MediaBrowser.Model.Entities; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public class JsonLowerCaseConverterTests + { + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() + { + Converters = + { + new JsonStringEnumConverter() + } + }; + + [Theory] + [InlineData(null, "{\"CollectionType\":null}")] + [InlineData(CollectionTypeOptions.Movies, "{\"CollectionType\":\"movies\"}")] + [InlineData(CollectionTypeOptions.MusicVideos, "{\"CollectionType\":\"musicvideos\"}")] + public void Serialize_CollectionTypeOptions_Correct(CollectionTypeOptions? collectionType, string expected) + { + Assert.Equal(expected, JsonSerializer.Serialize(new TestContainer(collectionType), _jsonOptions)); + } + + [Theory] + [InlineData("{\"CollectionType\":null}", null)] + [InlineData("{\"CollectionType\":\"movies\"}", CollectionTypeOptions.Movies)] + [InlineData("{\"CollectionType\":\"musicvideos\"}", CollectionTypeOptions.MusicVideos)] + public void Deserialize_CollectionTypeOptions_Correct(string json, CollectionTypeOptions? result) + { + var res = JsonSerializer.Deserialize(json, _jsonOptions); + Assert.NotNull(res); + Assert.Equal(result, res!.CollectionType); + } + + [Theory] + [InlineData(null)] + [InlineData(CollectionTypeOptions.Movies)] + [InlineData(CollectionTypeOptions.MusicVideos)] + public void RoundTrip_CollectionTypeOptions_Correct(CollectionTypeOptions? value) + { + var res = JsonSerializer.Deserialize(JsonSerializer.Serialize(new TestContainer(value), _jsonOptions), _jsonOptions); + Assert.NotNull(res); + Assert.Equal(value, res!.CollectionType); + } + + [Theory] + [InlineData("{\"CollectionType\":null}")] + [InlineData("{\"CollectionType\":\"movies\"}")] + [InlineData("{\"CollectionType\":\"musicvideos\"}")] + public void RoundTrip_String_Correct(string json) + { + var res = JsonSerializer.Serialize(JsonSerializer.Deserialize(json, _jsonOptions), _jsonOptions); + Assert.Equal(json, res); + } + + private class TestContainer + { + public TestContainer(CollectionTypeOptions? collectionType) + { + CollectionType = collectionType; + } + + [JsonConverter(typeof(JsonLowerCaseConverter))] + public CollectionTypeOptions? CollectionType { get; set; } + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonNullableGuidConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonNullableGuidConverterTests.cs new file mode 100644 index 000000000..b0dbc09e4 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonNullableGuidConverterTests.cs @@ -0,0 +1,80 @@ +using System; +using System.Text.Json; +using Jellyfin.Extensions.Json.Converters; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public class JsonNullableGuidConverterTests + { + private readonly JsonSerializerOptions _options; + + public JsonNullableGuidConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new JsonNullableGuidConverter()); + } + + [Fact] + public void Deserialize_Valid_Success() + { + Guid? value = JsonSerializer.Deserialize(@"""a852a27afe324084ae66db579ee3ee18""", _options); + Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value); + } + + [Fact] + public void Deserialize_ValidDashed_Success() + { + Guid? value = JsonSerializer.Deserialize(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", _options); + Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value); + } + + [Fact] + public void Roundtrip_Valid_Success() + { + Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18"); + string value = JsonSerializer.Serialize(guid, _options); + Assert.Equal(guid, JsonSerializer.Deserialize(value, _options)); + } + + [Fact] + public void Deserialize_Null_Null() + { + Assert.Null(JsonSerializer.Deserialize("null", _options)); + } + + [Fact] + public void Deserialize_EmptyGuid_EmptyGuid() + { + Assert.Equal(Guid.Empty, JsonSerializer.Deserialize(@"""00000000-0000-0000-0000-000000000000""", _options)); + } + + [Fact] + public void Serialize_EmptyGuid_Null() + { + Assert.Equal("null", JsonSerializer.Serialize((Guid?)Guid.Empty, _options)); + } + + [Fact] + public void Serialize_Null_Null() + { + Assert.Equal("null", JsonSerializer.Serialize((Guid?)null, _options)); + } + + [Fact] + public void Serialize_Valid_NoDash_Success() + { + var guid = (Guid?)new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA"); + var str = JsonSerializer.Serialize(guid, _options); + Assert.Equal($"\"{guid:N}\"", str); + } + + [Fact] + public void Serialize_Nullable_Success() + { + Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA"); + var str = JsonSerializer.Serialize(guid, _options); + Assert.Equal($"\"{guid:N}\"", str); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs new file mode 100644 index 000000000..655e07074 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs @@ -0,0 +1,38 @@ +using System.Text.Json; +using Jellyfin.Extensions.Json.Converters; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public class JsonStringConverterTests + { + private readonly JsonSerializerOptions _jsonSerializerOptions = new () + { + Converters = + { + new JsonStringConverter() + } + }; + + [Theory] + [InlineData("\"test\"", "test")] + [InlineData("123", "123")] + [InlineData("123.45", "123.45")] + [InlineData("true", "true")] + [InlineData("false", "false")] + public void Deserialize_String_Valid_Success(string input, string output) + { + var deserialized = JsonSerializer.Deserialize(input, _jsonSerializerOptions); + Assert.Equal(deserialized, output); + } + + [Fact] + public void Deserialize_Int32asInt32_Valid_Success() + { + const string? input = "123"; + const int output = 123; + var deserialized = JsonSerializer.Deserialize(input, _jsonSerializerOptions); + Assert.Equal(deserialized, output); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonVersionConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonVersionConverterTests.cs new file mode 100644 index 000000000..5fbac7eab --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonVersionConverterTests.cs @@ -0,0 +1,36 @@ +using System; +using System.Text.Json; +using Jellyfin.Extensions.Json.Converters; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public class JsonVersionConverterTests + { + private readonly JsonSerializerOptions _options; + + public JsonVersionConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new JsonVersionConverter()); + } + + [Fact] + public void Deserialize_Version_Success() + { + var input = "\"1.025.222\""; + var output = new Version(1, 25, 222); + var deserializedInput = JsonSerializer.Deserialize(input, _options); + Assert.Equal(output, deserializedInput); + } + + [Fact] + public void Serialize_Version_Success() + { + var input = new Version(1, 09, 59); + var output = "\"1.9.59\""; + var serializedInput = JsonSerializer.Serialize(input, _options); + Assert.Equal(output, serializedInput); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs new file mode 100644 index 000000000..ef135278f --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json.Converters; + +namespace Jellyfin.Extensions.Tests.Json.Models +{ + /// + /// The generic body model. + /// + /// The value type. + public class GenericBodyArrayModel + { + /// + /// Gets or sets the value. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")] + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public T[] Value { get; set; } = default!; + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs new file mode 100644 index 000000000..8e7b5a35b --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json.Converters; + +namespace Jellyfin.Extensions.Tests.Json.Models +{ + /// + /// The generic body IReadOnlyList model. + /// + /// The value type. + public class GenericBodyIReadOnlyListModel + { + /// + /// Gets or sets the value. + /// + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList Value { get; set; } = default!; + } +} diff --git a/tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs new file mode 100644 index 000000000..c72216d94 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs @@ -0,0 +1,21 @@ +using System; +using Xunit; + +namespace Jellyfin.Extensions.Tests +{ + public static class ShuffleExtensionsTests + { + private static readonly Random _rng = new Random(); + + [Fact] + public static void Shuffle_Valid_Correct() + { + byte[] original = new byte[1 << 6]; + _rng.NextBytes(original); + byte[] shuffled = (byte[])original.Clone(); + shuffled.Shuffle(); + + Assert.NotEqual(original, shuffled); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs new file mode 100644 index 000000000..d1aa2e476 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs @@ -0,0 +1,18 @@ +using System; +using Xunit; + +namespace Jellyfin.Extensions.Tests +{ + public class StringExtensionsTests + { + [Theory] + [InlineData("", '_', 0)] + [InlineData("___", '_', 3)] + [InlineData("test\x00", '\x00', 1)] + [InlineData("Imdb=tt0119567|Tmdb=330|TmdbCollection=328", '|', 2)] + public void ReadOnlySpan_Count_Success(string str, char needle, int count) + { + Assert.Equal(count, str.AsSpan().Count(needle)); + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs index 415682e85..45808375f 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs @@ -1,7 +1,7 @@ using System.IO; using System.Text.Json; using System.Threading.Tasks; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.MediaEncoding.Probing; using Xunit; diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 98fbb00d5..d8089eea2 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -2,7 +2,7 @@ using System; using System.Globalization; using System.IO; using System.Text.Json; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; diff --git a/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs deleted file mode 100644 index 955d296cc..000000000 --- a/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using MediaBrowser.Model.Entities; -using Xunit; - -namespace Jellyfin.Model.Tests.Entities -{ - public class JsonLowerCaseConverterTests - { - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() - { - Converters = - { - new JsonStringEnumConverter() - } - }; - - [Theory] - [InlineData(null, "{\"CollectionType\":null}")] - [InlineData(CollectionTypeOptions.Movies, "{\"CollectionType\":\"movies\"}")] - [InlineData(CollectionTypeOptions.MusicVideos, "{\"CollectionType\":\"musicvideos\"}")] - public void Serialize_CollectionTypeOptions_Correct(CollectionTypeOptions? collectionType, string expected) - { - Assert.Equal(expected, JsonSerializer.Serialize(new TestContainer(collectionType), _jsonOptions)); - } - - [Theory] - [InlineData("{\"CollectionType\":null}", null)] - [InlineData("{\"CollectionType\":\"movies\"}", CollectionTypeOptions.Movies)] - [InlineData("{\"CollectionType\":\"musicvideos\"}", CollectionTypeOptions.MusicVideos)] - public void Deserialize_CollectionTypeOptions_Correct(string json, CollectionTypeOptions? result) - { - var res = JsonSerializer.Deserialize(json, _jsonOptions); - Assert.NotNull(res); - Assert.Equal(result, res!.CollectionType); - } - - [Theory] - [InlineData(null)] - [InlineData(CollectionTypeOptions.Movies)] - [InlineData(CollectionTypeOptions.MusicVideos)] - public void RoundTrip_CollectionTypeOptions_Correct(CollectionTypeOptions? value) - { - var res = JsonSerializer.Deserialize(JsonSerializer.Serialize(new TestContainer(value), _jsonOptions), _jsonOptions); - Assert.NotNull(res); - Assert.Equal(value, res!.CollectionType); - } - - [Theory] - [InlineData("{\"CollectionType\":null}")] - [InlineData("{\"CollectionType\":\"movies\"}")] - [InlineData("{\"CollectionType\":\"musicvideos\"}")] - public void RoundTrip_String_Correct(string json) - { - var res = JsonSerializer.Serialize(JsonSerializer.Deserialize(json, _jsonOptions), _jsonOptions); - Assert.Equal(json, res); - } - - private class TestContainer - { - public TestContainer(CollectionTypeOptions? collectionType) - { - CollectionType = collectionType; - } - - [JsonConverter(typeof(JsonLowerCaseConverter))] - public CollectionTypeOptions? CollectionType { get; set; } - } - } -} diff --git a/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs b/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs new file mode 100644 index 000000000..25900bc09 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using MediaBrowser.Providers.Plugins.Omdb; +using Xunit; + +namespace Jellyfin.Providers.Tests.Omdb +{ + public class JsonOmdbConverterTests + { + private readonly JsonSerializerOptions _options; + + public JsonOmdbConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new JsonOmdbNotAvailableStringConverter()); + _options.Converters.Add(new JsonOmdbNotAvailableInt32Converter()); + _options.NumberHandling = JsonNumberHandling.AllowReadingFromString; + } + + [Fact] + public void Deserialize_Omdb_Response_Not_Available_Success() + { + const string Input = "{\"Title\":\"Chapter 1\",\"Year\":\"2013\",\"Rated\":\"TV-MA\",\"Released\":\"01 Feb 2013\",\"Season\":\"N/A\",\"Episode\":\"N/A\",\"Runtime\":\"55 min\",\"Genre\":\"Drama\",\"Director\":\"David Fincher\",\"Writer\":\"Michael Dobbs (based on the novels by), Andrew Davies (based on the mini-series by), Beau Willimon (created for television by), Beau Willimon, Sam Forman (staff writer)\",\"Actors\":\"Kevin Spacey, Robin Wright, Kate Mara, Corey Stoll\",\"Plot\":\"Congressman Francis Underwood has been declined the chair for Secretary of State. He's now gathering his own team to plot his revenge. Zoe Barnes, a reporter for the Washington Herald, will do anything to get her big break.\",\"Language\":\"English\",\"Country\":\"USA\",\"Awards\":\"N/A\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMTY5MTU4NDQzNV5BMl5BanBnXkFtZTgwMzk2ODcxMzE@._V1_SX300.jpg\",\"Ratings\":[{\"Source\":\"Internet Movie Database\",\"Value\":\"8.7/10\"}],\"Metascore\":\"N/A\",\"imdbRating\":\"8.7\",\"imdbVotes\":\"6736\",\"imdbID\":\"tt2161930\",\"seriesID\":\"N/A\",\"Type\":\"episode\",\"Response\":\"True\"}"; + var seasonRootObject = JsonSerializer.Deserialize(Input, _options); + Assert.NotNull(seasonRootObject); + Assert.Null(seasonRootObject?.Awards); + Assert.Null(seasonRootObject?.Episode); + Assert.Null(seasonRootObject?.Metascore); + } + + [Theory] + [InlineData("\"N/A\"")] + [InlineData("null")] + public void Deserialization_To_Nullable_Int_Shoud_Be_Null(string input) + { + var result = JsonSerializer.Deserialize(input, _options); + Assert.Null(result); + } + + [Theory] + [InlineData("\"8\"", 8)] + [InlineData("8", 8)] + public void Deserialize_NullableInt_Success(string input, int? expected) + { + var result = JsonSerializer.Deserialize(input, _options); + Assert.Equal(result, expected); + } + + [Theory] + [InlineData("\"N/A\"")] + [InlineData("null")] + public void Deserialization_To_Nullable_String_Shoud_Be_Null(string input) + { + var result = JsonSerializer.Deserialize(input, _options); + Assert.Null(result); + } + + [Theory] + [InlineData("\"Jellyfin\"", "Jellyfin")] + public void Deserialize_Normal_String_Success(string input, string expected) + { + var result = JsonSerializer.Deserialize(input, _options); + Assert.Equal(expected, result); + } + + [Fact] + public void Roundtrip_Valid_Success() + { + const string Input = "{\"Title\":\"Chapter 1\",\"Year\":\"2013\",\"Rated\":\"TV-MA\",\"Released\":\"01 Feb 2013\",\"Season\":\"N/A\",\"Episode\":\"N/A\",\"Runtime\":\"55 min\",\"Genre\":\"Drama\",\"Director\":\"David Fincher\",\"Writer\":\"Michael Dobbs (based on the novels by), Andrew Davies (based on the mini-series by), Beau Willimon (created for television by), Beau Willimon, Sam Forman (staff writer)\",\"Actors\":\"Kevin Spacey, Robin Wright, Kate Mara, Corey Stoll\",\"Plot\":\"Congressman Francis Underwood has been declined the chair for Secretary of State. He's now gathering his own team to plot his revenge. Zoe Barnes, a reporter for the Washington Herald, will do anything to get her big break.\",\"Language\":\"English\",\"Country\":\"USA\",\"Awards\":\"N/A\",\"Poster\":\"https://m.media-amazon.com/images/M/MV5BMTY5MTU4NDQzNV5BMl5BanBnXkFtZTgwMzk2ODcxMzE@._V1_SX300.jpg\",\"Ratings\":[{\"Source\":\"Internet Movie Database\",\"Value\":\"8.7/10\"}],\"Metascore\":\"N/A\",\"imdbRating\":\"8.7\",\"imdbVotes\":\"6736\",\"imdbID\":\"tt2161930\",\"seriesID\":\"N/A\",\"Type\":\"episode\",\"Response\":\"True\"}"; + var trip1 = JsonSerializer.Deserialize(Input, _options); + Assert.NotNull(trip1); + Assert.NotNull(trip1?.Title); + Assert.Null(trip1?.Awards); + Assert.Null(trip1?.Episode); + Assert.Null(trip1?.Metascore); + + var serializedTrip1 = JsonSerializer.Serialize(trip1!, _options); + var trip2 = JsonSerializer.Deserialize(serializedTrip1, _options); + Assert.NotNull(trip2); + Assert.NotNull(trip2?.Title); + Assert.Null(trip2?.Awards); + Assert.Null(trip2?.Episode); + Assert.Null(trip2?.Metascore); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index ea6838682..4ea05397d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -7,7 +7,7 @@ using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Models.StartupDtos; using Jellyfin.Api.Models.UserDtos; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using Xunit; namespace Jellyfin.Server.Integration.Tests diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index f5411dcb8..827365363 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -5,7 +5,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Models; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using Xunit; namespace Jellyfin.Server.Integration.Tests.Controllers diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs index 169a5a6c5..9c0fc72f6 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs @@ -6,7 +6,7 @@ using System.Net.Mime; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Models.StartupDtos; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using Xunit; using Xunit.Priority; diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 6584490de..8866ab53c 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -8,7 +8,7 @@ using System.Net.Mime; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Models.UserDtos; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Model.Dto; using Xunit; using Xunit.Priority; -- cgit v1.2.3 From 3beda02d925c74c7a7083eaee733537f3396ec92 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sun, 25 Jul 2021 00:52:16 +0800 Subject: add support for cuda tonemap and overlay --- .../MediaEncoding/EncodingHelper.cs | 385 ++++++++++++++++----- .../MediaEncoding/FilterOptionType.cs | 23 ++ .../MediaEncoding/IMediaEncoder.cs | 14 +- .../Encoder/EncoderValidator.cs | 102 +++++- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 39 ++- .../Probing/ProbeResultNormalizer.cs | 17 + 6 files changed, 477 insertions(+), 103 deletions(-) create mode 100644 MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 257cd5df6..b12cacb6f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -40,6 +40,8 @@ namespace MediaBrowser.Controller.MediaEncoding "ConstrainedHigh" }; + private static readonly Version minVersionForCudaOverlay = new Version(4, 4); + public EncodingHelper( IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder) @@ -109,17 +111,41 @@ namespace MediaBrowser.Controller.MediaEncoding private bool IsCudaSupported() { return _mediaEncoder.SupportsHwaccel("cuda") - && _mediaEncoder.SupportsFilter("scale_cuda", null) - && _mediaEncoder.SupportsFilter("yadif_cuda", null); + && _mediaEncoder.SupportsFilter("scale_cuda") + && _mediaEncoder.SupportsFilter("yadif_cuda") + && _mediaEncoder.SupportsFilter("hwupload_cuda"); } - private bool IsTonemappingSupported(EncodingJobInfo state, EncodingOptions options) + private bool IsOpenclTonemappingSupported(EncodingJobInfo state, EncodingOptions options) { var videoStream = state.VideoStream; - return IsColorDepth10(state) + if (videoStream == null) + { + return false; + } + + return (string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) + && IsColorDepth10(state) && _mediaEncoder.SupportsHwaccel("opencl") - && options.EnableTonemapping - && string.Equals(videoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase); + && _mediaEncoder.SupportsFilter("tonemap_opencl") + && options.EnableTonemapping; + } + + private bool IsCudaTonemappingSupported(EncodingJobInfo state, EncodingOptions options) + { + var videoStream = state.VideoStream; + if (videoStream == null) + { + return false; + } + + return (string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) + && IsColorDepth10(state) + && _mediaEncoder.SupportsHwaccel("cuda") + && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapCudaName) + && options.EnableTonemapping; } private bool IsVppTonemappingSupported(EncodingJobInfo state, EncodingOptions options) @@ -135,23 +161,25 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) { // Limited to HEVC for now since the filter doesn't accept master data from VP9. - return IsColorDepth10(state) + return string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + && IsColorDepth10(state) && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) && _mediaEncoder.SupportsHwaccel("vaapi") - && options.EnableVppTonemapping - && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase); + && _mediaEncoder.SupportsFilter("tonemap_vaapi") + && options.EnableVppTonemapping; } // Hybrid VPP tonemapping for QSV with VAAPI if (OperatingSystem.IsLinux() && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { // Limited to HEVC for now since the filter doesn't accept master data from VP9. - return IsColorDepth10(state) + return string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + && IsColorDepth10(state) && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) && _mediaEncoder.SupportsHwaccel("vaapi") + && _mediaEncoder.SupportsFilter("tonemap_vaapi") && _mediaEncoder.SupportsHwaccel("qsv") - && options.EnableVppTonemapping - && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase); + && options.EnableVppTonemapping; } // Native VPP tonemapping may come to QSV in the future. @@ -489,11 +517,14 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// Gets the input argument. /// - public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions) + public string GetInputArgument(EncodingJobInfo state, EncodingOptions options) { var arg = new StringBuilder(); - var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; - var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty; + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty; + var outputVideoCodec = GetVideoEncoder(state, options) ?? string.Empty; + var isWindows = OperatingSystem.IsWindows(); + var isLinux = OperatingSystem.IsLinux(); + var isMacOS = OperatingSystem.IsMacOS(); var isSwDecoder = string.IsNullOrEmpty(videoDecoder); var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; @@ -502,26 +533,24 @@ namespace MediaBrowser.Controller.MediaEncoding var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase); - var isWindows = OperatingSystem.IsWindows(); - var isLinux = OperatingSystem.IsLinux(); - var isMacOS = OperatingSystem.IsMacOS(); - var isTonemappingSupported = IsTonemappingSupported(state, encodingOptions); - var isVppTonemappingSupported = IsVppTonemappingSupported(state, encodingOptions); + var isCuvidVp9Decoder = videoDecoder.Contains("vp9_cuvid", StringComparison.OrdinalIgnoreCase); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); + var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options); if (!IsCopyCodec(outputVideoCodec)) { if (state.IsVideoRequest && _mediaEncoder.SupportsHwaccel("vaapi") - && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) { if (isVaapiDecoder) { - if (isTonemappingSupported && !isVppTonemappingSupported) + if (isOpenclTonemappingSupported && !isVppTonemappingSupported) { arg.Append("-init_hw_device vaapi=va:") - .Append(encodingOptions.VaapiDevice) - .Append(' ') - .Append("-init_hw_device opencl=ocl@va ") + .Append(options.VaapiDevice) + .Append(" -init_hw_device opencl=ocl@va ") .Append("-hwaccel_device va ") .Append("-hwaccel_output_format vaapi ") .Append("-filter_hw_device ocl "); @@ -530,14 +559,14 @@ namespace MediaBrowser.Controller.MediaEncoding { arg.Append("-hwaccel_output_format vaapi ") .Append("-vaapi_device ") - .Append(encodingOptions.VaapiDevice) + .Append(options.VaapiDevice) .Append(' '); } } else if (!isVaapiDecoder && isVaapiEncoder) { arg.Append("-vaapi_device ") - .Append(encodingOptions.VaapiDevice) + .Append(options.VaapiDevice) .Append(' '); } @@ -545,7 +574,7 @@ namespace MediaBrowser.Controller.MediaEncoding } if (state.IsVideoRequest - && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; @@ -581,9 +610,8 @@ namespace MediaBrowser.Controller.MediaEncoding else if (isVaapiDecoder && isVppTonemappingSupported) { arg.Append("-init_hw_device vaapi=va:") - .Append(encodingOptions.VaapiDevice) - .Append(' ') - .Append("-init_hw_device qsv@va ") + .Append(options.VaapiDevice) + .Append(" -init_hw_device qsv@va ") .Append("-hwaccel_output_format vaapi "); } @@ -592,7 +620,7 @@ namespace MediaBrowser.Controller.MediaEncoding } if (state.IsVideoRequest - && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) + && string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecDecoder) { // Fix for 'No decoder surfaces left' error. https://trac.ffmpeg.org/ticket/7562 @@ -600,22 +628,31 @@ namespace MediaBrowser.Controller.MediaEncoding } if (state.IsVideoRequest - && ((string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) - && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder)) - || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) - && (isD3d11vaDecoder || isSwDecoder)))) + && ((string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) + && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder)))) + { + if (!isCudaTonemappingSupported && isOpenclTonemappingSupported) + { + arg.Append("-init_hw_device opencl=ocl:") + .Append(options.OpenclDevice) + .Append(" -filter_hw_device ocl "); + } + } + + if (state.IsVideoRequest + && string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) + && (isD3d11vaDecoder || isSwDecoder)) { - if (isTonemappingSupported) + if (isOpenclTonemappingSupported) { arg.Append("-init_hw_device opencl=ocl:") - .Append(encodingOptions.OpenclDevice) - .Append(' ') - .Append("-filter_hw_device ocl "); + .Append(options.OpenclDevice) + .Append(" -filter_hw_device ocl "); } } if (state.IsVideoRequest - && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + && string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) { arg.Append("-hwaccel videotoolbox "); } @@ -1991,14 +2028,18 @@ namespace MediaBrowser.Controller.MediaEncoding var isQsvHevcEncoder = outputVideoCodec.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase); var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase); - var isTonemappingSupported = IsTonemappingSupported(state, options); - var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); + + var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion(); + var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= minVersionForCudaOverlay; + var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat); // Tonemapping and burn-in graphical subtitles requires overlay_vaapi. // But it's still in ffmpeg mailing list. Disable it for now. - if (isTonemappingSupportedOnVaapi && isTonemappingSupported && !isVppTonemappingSupported) + if (isTonemappingSupportedOnVaapi && isOpenclTonemappingSupported && !isVppTonemappingSupported) { return GetOutputSizeParam(state, options, outputVideoCodec); } @@ -2024,13 +2065,22 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(videoSizeParam) && !(isTonemappingSupportedOnQsv && isVppTonemappingSupported)) { - // For QSV, feed it into hardware encoder now + // upload graphical subtitle to QSV if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))) { videoSizeParam += ",hwupload=extra_hw_frames=64"; } } + + if (!string.IsNullOrEmpty(videoSizeParam)) + { + // upload graphical subtitle to cuda + if (isNvdecDecoder && isNvencEncoder && isCudaOverlaySupported && isCudaFormatConversionSupported) + { + videoSizeParam += ",hwupload_cuda"; + } + } } var mapPrefix = state.SubtitleStream.IsExternal ? @@ -2043,9 +2093,9 @@ namespace MediaBrowser.Controller.MediaEncoding // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference) // Always put the scaler before the overlay for better performance - var retStr = !outputSizeParam.IsEmpty - ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"" - : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""; + var retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""; // When the input may or may not be hardware VAAPI decodable if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) @@ -2056,9 +2106,9 @@ namespace MediaBrowser.Controller.MediaEncoding [sub]: SW scaling subtitle to FixedOutputSize [base][sub]: SW overlay */ - retStr = !outputSizeParam.IsEmpty - ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"" - : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""; + retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""; } // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first @@ -2071,9 +2121,9 @@ namespace MediaBrowser.Controller.MediaEncoding [sub]: SW scaling subtitle to FixedOutputSize [base][sub]: SW overlay */ - retStr = !outputSizeParam.IsEmpty - ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"" - : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""; + retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""; } else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) @@ -2090,16 +2140,25 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isLinux) { - retStr = !outputSizeParam.IsEmpty - ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" - : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\""; + retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\""; } } else if (isNvdecDecoder && isNvencEncoder) { - retStr = !outputSizeParam.IsEmpty - ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay,format=nv12|yuv420p,hwupload_cuda\"" - : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay,format=nv12|yuv420p,hwupload_cuda\""; + if (isCudaOverlaySupported && isCudaFormatConversionSupported) + { + retStr = outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]scale_cuda=format=yuv420p[base];[base][sub]overlay_cuda\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_cuda\""; + } + else + { + retStr = !outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay,format=nv12|yuv420p,hwupload_cuda\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay,format=nv12|yuv420p,hwupload_cuda\""; + } } return string.Format( @@ -2196,11 +2255,11 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiHevcEncoder = videoEncoder.Contains("hevc_vaapi", StringComparison.OrdinalIgnoreCase); var isQsvH264Encoder = videoEncoder.Contains("h264_qsv", StringComparison.OrdinalIgnoreCase); var isQsvHevcEncoder = videoEncoder.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase); - var isTonemappingSupported = IsTonemappingSupported(state, options); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder); - var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported)) + var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isOpenclTonemappingSupported || isVppTonemappingSupported)) || (isTonemappingSupportedOnQsv && isVppTonemappingSupported); var outputPixFmt = "format=nv12"; @@ -2251,15 +2310,23 @@ namespace MediaBrowser.Controller.MediaEncoding var outputWidth = width.Value; var outputHeight = height.Value; - var isTonemappingSupported = IsTonemappingSupported(state, options); + var isNvencEncoder = videoEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); + var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options); var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase); - var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilter("scale_cuda", "Output format (default \"same\")"); + var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion(); + var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= minVersionForCudaOverlay; + var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat); + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var outputPixFmt = string.Empty; if (isCudaFormatConversionSupported) { - outputPixFmt = "format=nv12"; - if (isTonemappingSupported && isTonemappingSupportedOnNvenc) + outputPixFmt = (hasGraphicalSubs && isCudaOverlaySupported && isNvencEncoder) + ? "format=yuv420p" + : "format=nv12"; + if ((isOpenclTonemappingSupported || isCudaTonemappingSupported) + && isTonemappingSupportedOnNvenc) { outputPixFmt = "format=p010"; } @@ -2525,16 +2592,21 @@ namespace MediaBrowser.Controller.MediaEncoding var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase); var isCuvidH264Decoder = videoDecoder.Contains("h264_cuvid", StringComparison.OrdinalIgnoreCase); var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase); + var isCuvidVp9Decoder = videoDecoder.Contains("vp9_cuvid", StringComparison.OrdinalIgnoreCase); var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1; var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1; var isLinux = OperatingSystem.IsLinux(); var isColorDepth10 = IsColorDepth10(state); - var isTonemappingSupported = IsTonemappingSupported(state, options); - var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); - var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder); + + var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder); var isTonemappingSupportedOnAmf = string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && (isD3d11vaDecoder || isSwDecoder); var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder); + var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options); + var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); + var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options); + var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion(); + var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= minVersionForCudaOverlay; var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; @@ -2546,19 +2618,25 @@ namespace MediaBrowser.Controller.MediaEncoding var isScalingInAdvance = false; var isCudaDeintInAdvance = false; var isHwuploadCudaRequired = false; + var isNoTonemapFilterApplied = true; var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); // Add OpenCL tonemapping filter for NVENC/AMF/VAAPI. - if (isTonemappingSupportedOnNvenc || isTonemappingSupportedOnAmf || (isTonemappingSupportedOnVaapi && !isVppTonemappingSupported)) + if ((isTonemappingSupportedOnNvenc && !isCudaTonemappingSupported) || isTonemappingSupportedOnAmf || (isTonemappingSupportedOnVaapi && !isVppTonemappingSupported)) { - // Currently only with the use of NVENC decoder can we get a decent performance. - // Currently only the HEVC/H265 format is supported with NVDEC decoder. // NVIDIA Pascal and Turing or higher are recommended. // AMD Polaris and Vega or higher are recommended. // Intel Kaby Lake or newer is required. - if (isTonemappingSupported) + if (isOpenclTonemappingSupported) { + isNoTonemapFilterApplied = false; + var inputHdrParams = GetInputHdrParams(videoStream.ColorTransfer); + if (!string.IsNullOrEmpty(inputHdrParams)) + { + filters.Add(inputHdrParams); + } + var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}"; if (options.TonemappingParam != 0) @@ -2630,7 +2708,11 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add("hwdownload,format=p010"); } - if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder) + if (isNvdecDecoder + || isCuvidHevcDecoder + || isCuvidVp9Decoder + || isSwDecoder + || isD3d11vaDecoder) { // Upload the HDR10 or HLG data to the OpenCL device, // use tonemap_opencl filter for tone mapping, @@ -2638,6 +2720,14 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add("hwupload"); } + // Fallback to hable if bt2390 is chosen but not supported in tonemap_opencl. + var isBt2390SupportedInOpenclTonemap = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapOpenclBt2390); + if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase) + && !isBt2390SupportedInOpenclTonemap) + { + options.TonemappingAlgorithm = "hable"; + } + filters.Add( string.Format( CultureInfo.InvariantCulture, @@ -2649,7 +2739,11 @@ namespace MediaBrowser.Controller.MediaEncoding options.TonemappingParam, options.TonemappingRange)); - if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder) + if (isNvdecDecoder + || isCuvidHevcDecoder + || isCuvidVp9Decoder + || isSwDecoder + || isD3d11vaDecoder) { filters.Add("hwdownload"); filters.Add("format=nv12"); @@ -2665,12 +2759,18 @@ namespace MediaBrowser.Controller.MediaEncoding // Reverse the data route from opencl to vaapi. filters.Add("hwmap=derive_device=vaapi:reverse=1"); } + + var outputSdrParams = GetOutputSdrParams(options.TonemappingRange); + if (!string.IsNullOrEmpty(outputSdrParams)) + { + filters.Add(outputSdrParams); + } } } // When the input may or may not be hardware VAAPI decodable. if ((isVaapiH264Encoder || isVaapiHevcEncoder) - && !(isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported))) + && !(isTonemappingSupportedOnVaapi && (isOpenclTonemappingSupported || isVppTonemappingSupported))) { filters.Add("format=nv12|vaapi"); filters.Add("hwupload"); @@ -2778,6 +2878,61 @@ namespace MediaBrowser.Controller.MediaEncoding request.MaxHeight)); } + // Add Cuda tonemapping filter. + if (isNvdecDecoder && isCudaTonemappingSupported) + { + isNoTonemapFilterApplied = false; + var inputHdrParams = GetInputHdrParams(videoStream.ColorTransfer); + if (!string.IsNullOrEmpty(inputHdrParams)) + { + filters.Add(inputHdrParams); + } + + var parameters = (hasGraphicalSubs && isCudaOverlaySupported && isNvencEncoder) + ? "tonemap_cuda=format=yuv420p:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:peak={1}:desat={2}" + : "tonemap_cuda=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:peak={1}:desat={2}"; + + if (options.TonemappingParam != 0) + { + parameters += ":param={3}"; + } + + if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) + { + parameters += ":range={4}"; + } + + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + parameters, + options.TonemappingAlgorithm, + options.TonemappingPeak, + options.TonemappingDesat, + options.TonemappingParam, + options.TonemappingRange)); + + if (isLibX264Encoder + || isLibX265Encoder + || hasTextSubs + || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder)) + { + if (isNvencEncoder) + { + isHwuploadCudaRequired = true; + } + + filters.Add("hwdownload"); + filters.Add("format=nv12"); + } + + var outputSdrParams = GetOutputSdrParams(options.TonemappingRange); + if (!string.IsNullOrEmpty(outputSdrParams)) + { + filters.Add(outputSdrParams); + } + } + // Add VPP tonemapping filter for VAAPI. // Full hardware based video post processing, faster than OpenCL but lacks fine tuning options. if ((isTonemappingSupportedOnVaapi || isTonemappingSupportedOnQsv) @@ -2787,10 +2942,10 @@ namespace MediaBrowser.Controller.MediaEncoding } // Another case is when using Nvenc decoder. - if (isNvdecDecoder && !isTonemappingSupported) + if (isNvdecDecoder && !isOpenclTonemappingSupported && !isCudaTonemappingSupported) { var codec = videoStream.Codec; - var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilter("scale_cuda", "Output format (default \"same\")"); + var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat); // Assert 10-bit hardware decodable if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) @@ -2799,7 +2954,10 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isCudaFormatConversionSupported) { - if (isLibX264Encoder || isLibX265Encoder || hasSubs) + if (isLibX264Encoder + || isLibX265Encoder + || hasTextSubs + || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder)) { if (isNvencEncoder) { @@ -2826,7 +2984,11 @@ namespace MediaBrowser.Controller.MediaEncoding } // Assert 8-bit hardware decodable - else if (!isColorDepth10 && (isLibX264Encoder || isLibX265Encoder || hasSubs)) + else if (!isColorDepth10 + && (isLibX264Encoder + || isLibX265Encoder + || hasTextSubs + || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder))) { if (isNvencEncoder) { @@ -2847,7 +3009,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // Convert hw context from ocl to va. // For tonemapping and text subs burn-in. - if (isTonemappingSupportedOnVaapi && isTonemappingSupported && !isVppTonemappingSupported) + if (isTonemappingSupportedOnVaapi && isOpenclTonemappingSupported && !isVppTonemappingSupported) { filters.Add("scale_vaapi"); } @@ -2893,6 +3055,17 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add("hwupload_cuda"); } + // If no tonemap filter is applied, + // tag the video range as SDR to prevent the encoder from encoding HDR video. + if (isNoTonemapFilterApplied) + { + var outputSdrParams = GetOutputSdrParams(null); + if (!string.IsNullOrEmpty(outputSdrParams)) + { + filters.Add(outputSdrParams); + } + } + var output = string.Empty; if (filters.Count > 0) { @@ -2905,6 +3078,36 @@ namespace MediaBrowser.Controller.MediaEncoding return output; } + public static string GetInputHdrParams(string colorTransfer) + { + if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) + { + // HLG + return "setparams=color_primaries=bt2020:color_trc=arib-std-b67:colorspace=bt2020nc"; + } + else + { + // HDR10 + return "setparams=color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc"; + } + } + + public static string GetOutputSdrParams(string tonemappingRange) + { + // SDR + if (string.Equals(tonemappingRange, "tv", StringComparison.OrdinalIgnoreCase)) + { + return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=tv"; + } + + if (string.Equals(tonemappingRange, "pc", StringComparison.OrdinalIgnoreCase)) + { + return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=pc"; + } + + return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709"; + } + /// /// Gets the number of threads. /// @@ -3371,8 +3574,13 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && IsVppTonemappingSupported(state, encodingOptions)) { - // Since tonemap_vaapi only support HEVC for now, no need to check the codec again. - return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10); + var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty; + var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; + if (isQsvEncoder) + { + // Since tonemap_vaapi only support HEVC for now, no need to check the codec again. + return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10); + } } if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) @@ -3895,6 +4103,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (videoStream != null) { + if (videoStream.BitDepth.HasValue) + { + return videoStream.BitDepth.Value == 10; + } + if (!string.IsNullOrEmpty(videoStream.PixelFormat)) { result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase); @@ -3914,12 +4127,6 @@ namespace MediaBrowser.Controller.MediaEncoding return true; } } - - result = (videoStream.BitDepth ?? 8) == 10; - if (result) - { - return true; - } } return result; diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs new file mode 100644 index 000000000..7ce707b19 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Controller.MediaEncoding +{ + /// + /// Enum FilterOptionType. + /// + public enum FilterOptionType + { + /// + /// The scale_cuda_format. + /// + ScaleCudaFormat = 0, + + /// + /// The tonemap_cuda_name. + /// + TonemapCudaName = 1, + + /// + /// The tonemap_opencl_bt2390. + /// + TonemapOpenclBt2390 = 2 + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 76a9fd7c7..31913acef 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -55,9 +55,21 @@ namespace MediaBrowser.Controller.MediaEncoding /// Whether given filter is supported. /// /// The filter. + /// true if the filter is supported, false otherwise. + bool SupportsFilter(string filter); + + /// + /// Whether filter is supported with the given option. + /// /// The option. /// true if the filter is supported, false otherwise. - bool SupportsFilter(string filter, string option); + bool SupportsFilterWithOption(FilterOptionType option); + + /// + /// Get the version of media encoder. + /// + /// The version of media encoder. + Version GetMediaEncoderVersion(); /// /// Extracts the audio image. diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f782e65bd..1ec159c9a 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -89,6 +89,24 @@ namespace MediaBrowser.MediaEncoding.Encoder "hevc_videotoolbox" }; + private static readonly string[] _requiredFilters = new[] + { + "scale_cuda", + "yadif_cuda", + "hwupload_cuda", + "overlay_cuda", + "tonemap_cuda", + "tonemap_opencl", + "tonemap_vaapi", + }; + + private static readonly IReadOnlyDictionary _filterOptionsDict = new Dictionary + { + { 0, new string[] { "scale_cuda", "Output format (default \"same\")" } }, + { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } }, + { 2, new string[] { "tonemap_opencl", "bt2390" } } + }; + // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below private static readonly IReadOnlyDictionary _ffmpegMinimumLibraryVersions = new Dictionary { @@ -156,7 +174,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } // Work out what the version under test is - var version = GetFFmpegVersion(versionOutput); + var version = GetFFmpegVersionInternal(versionOutput); _logger.LogInformation("Found ffmpeg version {Version}", version != null ? version.ToString() : "unknown"); @@ -200,6 +218,34 @@ namespace MediaBrowser.MediaEncoding.Encoder public IEnumerable GetHwaccels() => GetHwaccelTypes(); + public IEnumerable GetFilters() => GetFFmpegFilters(); + + public IDictionary GetFiltersWithOption() => GetFFmpegFiltersWithOption(); + + public Version? GetFFmpegVersion() + { + string output; + try + { + output = GetProcessOutput(_encoderPath, "-version"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error validating encoder"); + return null; + } + + if (string.IsNullOrWhiteSpace(output)) + { + _logger.LogError("FFmpeg validation: The process returned no result"); + return null; + } + + _logger.LogDebug("ffmpeg output: {Output}", output); + + return GetFFmpegVersionInternal(output); + } + /// /// Using the output from "ffmpeg -version" work out the FFmpeg version. /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy @@ -208,7 +254,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// The output from "ffmpeg -version". /// The FFmpeg version. - internal Version? GetFFmpegVersion(string output) + internal Version? GetFFmpegVersionInternal(string output) { // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)"); @@ -297,9 +343,9 @@ namespace MediaBrowser.MediaEncoding.Encoder return found; } - public bool CheckFilter(string filter, string option) + public bool CheckFilterWithOption(string filter, string option) { - if (string.IsNullOrEmpty(filter)) + if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option)) { return false; } @@ -317,11 +363,6 @@ namespace MediaBrowser.MediaEncoding.Encoder if (output.Contains("Filter " + filter, StringComparison.Ordinal)) { - if (string.IsNullOrEmpty(option)) - { - return true; - } - return output.Contains(option, StringComparison.Ordinal); } @@ -362,6 +403,49 @@ namespace MediaBrowser.MediaEncoding.Encoder return found; } + private IEnumerable GetFFmpegFilters() + { + string output; + try + { + output = GetProcessOutput(_encoderPath, "-filters"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting available filters"); + return Enumerable.Empty(); + } + + if (string.IsNullOrWhiteSpace(output)) + { + return Enumerable.Empty(); + } + + var found = Regex + .Matches(output, @"^\s\S{3}\s(?[\w|-]+)\s+.+$", RegexOptions.Multiline) + .Cast() + .Select(x => x.Groups["filter"].Value) + .Where(x => _requiredFilters.Contains(x)); + + _logger.LogInformation("Available filters: {Filters}", found); + + return found; + } + + private IDictionary GetFFmpegFiltersWithOption() + { + IDictionary dict = new Dictionary(); + for (int i = 0; i < _filterOptionsDict.Count; i++) + { + if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2) + { + dict.Add(i, CheckFilterWithOption(val[0], val[1])); + } + } + + return dict; + } + private string GetProcessOutput(string path, string arguments) { using (var process = new Process() diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 412a95321..238627e96 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -66,7 +66,10 @@ namespace MediaBrowser.MediaEncoding.Encoder private List _encoders = new List(); private List _decoders = new List(); private List _hwaccels = new List(); + private List _filters = new List(); + private IDictionary _filtersWithOption = new Dictionary(); + private Version _ffmpegVersion = null; private string _ffmpegPath = string.Empty; private string _ffprobePath; private int threads; @@ -130,7 +133,11 @@ namespace MediaBrowser.MediaEncoding.Encoder SetAvailableDecoders(validator.GetDecoders()); SetAvailableEncoders(validator.GetEncoders()); + SetAvailableFilters(validator.GetFilters()); + SetAvailableFiltersWithOption(validator.GetFiltersWithOption()); SetAvailableHwaccels(validator.GetHwaccels()); + SetMediaEncoderVersion(validator); + threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null); } @@ -278,6 +285,21 @@ namespace MediaBrowser.MediaEncoding.Encoder _hwaccels = list.ToList(); } + public void SetAvailableFilters(IEnumerable list) + { + _filters = list.ToList(); + } + + public void SetAvailableFiltersWithOption(IDictionary dict) + { + _filtersWithOption = dict; + } + + public void SetMediaEncoderVersion(EncoderValidator validator) + { + _ffmpegVersion = validator.GetFFmpegVersion(); + } + public bool SupportsEncoder(string encoder) { return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase); @@ -293,17 +315,26 @@ namespace MediaBrowser.MediaEncoding.Encoder return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase); } - public bool SupportsFilter(string filter, string option) + public bool SupportsFilter(string filter) { - if (_ffmpegPath != null) + return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase); + } + + public bool SupportsFilterWithOption(FilterOptionType option) + { + if (_filtersWithOption.TryGetValue((int)option, out var val)) { - var validator = new EncoderValidator(_logger, _ffmpegPath); - return validator.CheckFilter(filter, option); + return val; } return false; } + public Version GetMediaEncoderVersion() + { + return _ffmpegVersion; + } + public bool CanEncodeToAudioCodec(string codec) { if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index c9ad3c41e..d6799a170 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -739,6 +739,23 @@ namespace MediaBrowser.MediaEncoding.Probing stream.BitDepth = streamInfo.BitsPerRawSample; } + if (!stream.BitDepth.HasValue) + { + if (!string.IsNullOrEmpty(streamInfo.PixelFormat) + && streamInfo.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase)) + { + stream.BitDepth = 10; + } + + if (!string.IsNullOrEmpty(streamInfo.Profile) + && (streamInfo.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) + || streamInfo.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase) + || streamInfo.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase))) + { + stream.BitDepth = 10; + } + } + // stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) || // string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) || // string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase); -- cgit v1.2.3 From 2345646ff159f474588e24c29ab94a97a4f41e6d Mon Sep 17 00:00:00 2001 From: boolemancer Date: Sat, 14 Aug 2021 02:55:51 -0700 Subject: Fix explicit stream selection in MediaEncoder.ExtractImageInternal --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 12 ++---------- MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs | 18 +----------------- 2 files changed, 3 insertions(+), 27 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 412a95321..e0a8102f9 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -503,15 +503,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var inputArgument = GetInputArgument(inputFile, mediaSource); - if (isAudio) - { - if (imageStreamIndex.HasValue && imageStreamIndex.Value > 0) - { - // It seems for audio files we need to subtract 1 (for the audio stream??) - imageStreamIndex = imageStreamIndex.Value - 1; - } - } - else + if (!isAudio) { // The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter. try @@ -582,7 +574,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _ => string.Empty }; - var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; + var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; var enableHdrExtraction = allowTonemap && string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase); if (enableHdrExtraction) diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index 30af6710a..453938be7 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -88,22 +88,6 @@ namespace MediaBrowser.Providers.MediaInfo if (imageStream != null) { - // Instead of using the raw stream index, we need to use nth video/embedded image stream - var videoIndex = -1; - foreach (var mediaStream in mediaStreams) - { - if (mediaStream.Type == MediaStreamType.Video || - mediaStream.Type == MediaStreamType.EmbeddedImage) - { - videoIndex++; - } - - if (mediaStream == imageStream) - { - break; - } - } - MediaSourceInfo mediaSource = new MediaSourceInfo { VideoType = item.VideoType, @@ -111,7 +95,7 @@ namespace MediaBrowser.Providers.MediaInfo Protocol = item.PathProtocol.Value, }; - extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, videoIndex, cancellationToken).ConfigureAwait(false); + extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, imageStream.Index, cancellationToken).ConfigureAwait(false); } else { -- cgit v1.2.3 From 963ab2dab6a8c4dafadae61312b36ed5fbb1f323 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 4 Apr 2021 23:02:28 +0200 Subject: Simplify the way we choose our ffmpeg * no longer search $PATH * no longer require a full path * don't fall back --- Emby.Server.Implementations/ApplicationHost.cs | 1 - .../MediaEncoding/IMediaEncoder.cs | 6 -- .../Encoder/EncoderValidator.cs | 4 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 107 ++++++--------------- MediaBrowser.Model/System/SystemInfo.cs | 1 + .../EncoderValidatorTests.cs | 8 +- 6 files changed, 38 insertions(+), 89 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index bf7ddace2..0b5322f39 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1099,7 +1099,6 @@ namespace Emby.Server.Implementations ServerName = FriendlyName, LocalAddress = GetSmartApiUrl(source), SupportsLibraryMonitor = true, - EncoderLocation = _mediaEncoder.EncoderLocation, SystemArchitecture = RuntimeInformation.OSArchitecture, PackageName = _startupOptions.PackageName }; diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index ff2456070..63308fa09 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -10,7 +10,6 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.System; namespace MediaBrowser.Controller.MediaEncoding { @@ -19,11 +18,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// public interface IMediaEncoder : ITranscoderSupport { - /// - /// Gets location of the discovered FFmpeg tool. - /// - FFmpegLocation EncoderLocation { get; } - /// /// Gets the encoder path. /// diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f782e65bd..ef831ab82 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -12,8 +12,6 @@ namespace MediaBrowser.MediaEncoding.Encoder { public class EncoderValidator { - private const string DefaultEncoderPath = "ffmpeg"; - private static readonly string[] _requiredDecoders = new[] { "h264", @@ -106,7 +104,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly string _encoderPath; - public EncoderValidator(ILogger logger, string encoderPath = DefaultEncoderPath) + public EncoderValidator(ILogger logger, string encoderPath) { _logger = logger; _encoderPath = encoderPath; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 412a95321..f8ba78e46 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -23,7 +23,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -69,7 +68,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private string _ffmpegPath = string.Empty; private string _ffprobePath; - private int threads; + private int _threads; public MediaEncoder( ILogger logger, @@ -89,9 +88,6 @@ namespace MediaBrowser.MediaEncoding.Encoder /// public string EncoderPath => _ffmpegPath; - /// - public FFmpegLocation EncoderLocation { get; private set; } - /// /// Run at startup or if the user removes a Custom path from transcode page. /// Sets global variables FFmpegPath. @@ -100,20 +96,23 @@ namespace MediaBrowser.MediaEncoding.Encoder public void SetFFmpegPath() { // 1) Custom path stored in config/encoding xml file under tag takes precedence - if (!ValidatePath(_configurationManager.GetEncodingOptions().EncoderAppPath, FFmpegLocation.Custom)) + var ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath; + if (string.IsNullOrEmpty(ffmpegPath)) { // 2) Check if the --ffmpeg CLI switch has been given - if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument)) + ffmpegPath = _startupOptionFFmpegPath; + if (string.IsNullOrEmpty(ffmpegPath)) { - // 3) Search system $PATH environment variable for valid FFmpeg - if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System)) - { - EncoderLocation = FFmpegLocation.NotFound; - _ffmpegPath = null; - } + // 3) Check "ffmpeg" + ffmpegPath = "ffmpeg"; } } + if (!ValidatePath(ffmpegPath)) + { + _ffmpegPath = null; + } + // Write the FFmpeg path to the config/encoding.xml file as so it appears in UI var config = _configurationManager.GetEncodingOptions(); config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty; @@ -131,10 +130,10 @@ namespace MediaBrowser.MediaEncoding.Encoder SetAvailableDecoders(validator.GetDecoders()); SetAvailableEncoders(validator.GetEncoders()); SetAvailableHwaccels(validator.GetHwaccels()); - threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null); + _threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null); } - _logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty); + _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty); } /// @@ -153,15 +152,12 @@ namespace MediaBrowser.MediaEncoding.Encoder { throw new ArgumentException("Unexpected pathType value"); } - else if (string.IsNullOrWhiteSpace(path)) + + if (string.IsNullOrWhiteSpace(path)) { // User had cleared the custom path in UI newPath = string.Empty; } - else if (File.Exists(path)) - { - newPath = path; - } else if (Directory.Exists(path)) { // Given path is directory, so resolve down to filename @@ -169,7 +165,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } else { - throw new ResourceNotFoundException(); + newPath = path; } // Write the new ffmpeg path to the xml as @@ -184,37 +180,26 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// Validates the supplied FQPN to ensure it is a ffmpeg utility. - /// If checks pass, global variable FFmpegPath and EncoderLocation are updated. + /// If checks pass, global variable FFmpegPath is updated. /// /// FQPN to test. - /// Location (External, Custom, System) of tool. /// true if the version validation succeeded; otherwise, false. - private bool ValidatePath(string path, FFmpegLocation location) + private bool ValidatePath(string path) { - bool rc = false; - - if (!string.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) { - if (File.Exists(path)) - { - rc = new EncoderValidator(_logger, path).ValidateVersion(); - - if (!rc) - { - _logger.LogWarning("FFmpeg: {Location}: Failed version check: {Path}", location, path); - } + return false; + } - _ffmpegPath = path; - EncoderLocation = location; - return true; - } - else - { - _logger.LogWarning("FFmpeg: {Location}: File not found: {Path}", location, path); - } + bool rc = new EncoderValidator(_logger, path).ValidateVersion(); + if (!rc) + { + _logger.LogWarning("FFmpeg: Failed version check: {Path}", path); + return false; } - return rc; + _ffmpegPath = path; + return true; } private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false) @@ -235,34 +220,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - /// - /// Search the system $PATH environment variable looking for given filename. - /// - /// The filename. - /// The full path to the file. - private string ExistsOnSystemPath(string fileName) - { - var inJellyfinPath = GetEncoderPathFromDirectory(AppContext.BaseDirectory, fileName, recursive: true); - if (!string.IsNullOrEmpty(inJellyfinPath)) - { - return inJellyfinPath; - } - - var values = Environment.GetEnvironmentVariable("PATH"); - - foreach (var path in values.Split(Path.PathSeparator)) - { - var candidatePath = GetEncoderPathFromDirectory(path, fileName); - - if (!string.IsNullOrEmpty(candidatePath)) - { - return candidatePath; - } - } - - return null; - } - public void SetAvailableEncoders(IEnumerable list) { _encoders = list.ToList(); @@ -394,7 +351,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = extractChapters ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format" : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format"; - args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, threads).Trim(); + args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim(); var process = new Process { @@ -615,7 +572,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads); + var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads); if (offset.HasValue) { @@ -728,7 +685,7 @@ namespace MediaBrowser.MediaEncoding.Encoder Directory.CreateDirectory(targetDirectory); var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg"); - var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads); + var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, _threads); if (!string.IsNullOrWhiteSpace(container)) { diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index e45b2f33a..a82c1c8c0 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -133,6 +133,7 @@ namespace MediaBrowser.Model.System [Obsolete("This should be handled by the package manager")] public bool HasUpdateAvailable { get; set; } + [Obsolete("This isn't set correctly anymore")] public FFmpegLocation EncoderLocation { get; set; } public Architecture SystemArchitecture { get; set; } diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs index 39fd8afda..cc429b442 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs @@ -9,12 +9,13 @@ namespace Jellyfin.MediaEncoding.Tests { public class EncoderValidatorTests { + private readonly EncoderValidator _encoderValidator = new EncoderValidator(new NullLogger(), "ffmpeg"); + [Theory] [ClassData(typeof(GetFFmpegVersionTestData))] public void GetFFmpegVersionTest(string versionOutput, Version? version) { - var val = new EncoderValidator(new NullLogger()); - Assert.Equal(version, val.GetFFmpegVersion(versionOutput)); + Assert.Equal(version, _encoderValidator.GetFFmpegVersion(versionOutput)); } [Theory] @@ -28,8 +29,7 @@ namespace Jellyfin.MediaEncoding.Tests [InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput, false)] public void ValidateVersionInternalTest(string versionOutput, bool valid) { - var val = new EncoderValidator(new NullLogger()); - Assert.Equal(valid, val.ValidateVersionInternal(versionOutput)); + Assert.Equal(valid, _encoderValidator.ValidateVersionInternal(versionOutput)); } private class GetFFmpegVersionTestData : IEnumerable -- cgit v1.2.3 From 52585a865e921196c6e47fe18b19b63107398d8d Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 21 Sep 2021 13:28:34 +0200 Subject: Validate ffmpeg path --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index a7bcaf544..4cbd1bbc8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -165,14 +165,22 @@ namespace MediaBrowser.MediaEncoding.Encoder // User had cleared the custom path in UI newPath = string.Empty; } - else if (Directory.Exists(path)) - { - // Given path is directory, so resolve down to filename - newPath = GetEncoderPathFromDirectory(path, "ffmpeg"); - } else { - newPath = path; + if (Directory.Exists(path)) + { + // Given path is directory, so resolve down to filename + newPath = GetEncoderPathFromDirectory(path, "ffmpeg"); + } + else + { + newPath = path; + } + + if (!new EncoderValidator(_logger, newPath).ValidateVersion()) + { + throw new ResourceNotFoundException(); + } } // Write the new ffmpeg path to the xml as -- cgit v1.2.3 From 95f344722ce34dde75408bce4138fe47b7eef6d0 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 11 Sep 2021 16:20:49 +0200 Subject: Don't set ffmpeg path from null to its Display value + add 404 --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4cbd1bbc8..057293b66 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -151,6 +151,16 @@ namespace MediaBrowser.MediaEncoding.Encoder /// The path type. public void UpdateEncoderPath(string path, string pathType) { + var config = _configurationManager.GetEncodingOptions(); + + // Filesystem may not be case insensitive, but EncoderAppPathDisplay should always point to a valid file? + if (string.IsNullOrEmpty(config.EncoderAppPath) + && string.Equals(config.EncoderAppPathDisplay, path, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Existing ffmpeg path is empty and the new path is the same as {EncoderAppPathDisplay}. Skipping", nameof(config.EncoderAppPathDisplay)); + return; + } + string newPath; _logger.LogInformation("Attempting to update encoder path to {Path}. pathType: {PathType}", path ?? string.Empty, pathType ?? string.Empty); @@ -183,9 +193,14 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + // Don't save an invalid path + if (!File.Exists(path)) + { + throw new FileNotFoundException(); + } + // Write the new ffmpeg path to the xml as // This ensures its not lost on next startup - var config = _configurationManager.GetEncodingOptions(); config.EncoderAppPath = newPath; _configurationManager.SaveConfiguration("encoding", config); -- cgit v1.2.3 From a353081ea33b3f635e9472371f228bab63669347 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Tue, 21 Sep 2021 22:54:11 +0200 Subject: Update MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 ------ 1 file changed, 6 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 057293b66..22ed9a8eb 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -193,12 +193,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - // Don't save an invalid path - if (!File.Exists(path)) - { - throw new FileNotFoundException(); - } - // Write the new ffmpeg path to the xml as // This ensures its not lost on next startup config.EncoderAppPath = newPath; -- cgit v1.2.3 From 9234e5bf80194e45acac25c60cb76f401bffaf96 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sun, 26 Sep 2021 08:14:36 -0600 Subject: Remove all instances of en-US culture --- Emby.Dlna/Didl/DidlBuilder.cs | 28 +++++++-------- Emby.Dlna/Eventing/DlnaEventManager.cs | 8 ++--- Emby.Dlna/PlayTo/Device.cs | 12 +++---- Emby.Dlna/PlayTo/PlayToController.cs | 8 ++--- Emby.Dlna/PlayTo/SsdpHttpClient.cs | 8 ++--- Emby.Dlna/Server/DescriptionXmlBuilder.cs | 5 ++- .../MediaEncoder/EncodingManager.cs | 3 +- Jellyfin.Api/Controllers/ImageController.cs | 2 +- Jellyfin.Api/Controllers/SubtitleController.cs | 2 +- MediaBrowser.Controller/Entities/Year.cs | 4 +-- .../MediaEncoding/EncodingHelper.cs | 42 +++++++++++----------- MediaBrowser.Controller/MediaEncoding/JobLogger.cs | 11 +++--- .../Images/LocalImageProvider.cs | 4 +-- .../Parsers/BaseItemXmlParser.cs | 8 ++--- MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs | 12 +++---- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 11 ++---- .../Probing/ProbeResultNormalizer.cs | 27 +++++++------- .../Globalization/ILocalizationManager.cs | 4 +-- MediaBrowser.Providers/Manager/ImageSaver.cs | 18 +++++----- .../Plugins/Omdb/OmdbProvider.cs | 13 ++++--- MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs | 10 +++--- .../Parsers/EpisodeNfoParser.cs | 10 +++--- .../Savers/EpisodeNfoSaver.cs | 18 +++++----- 23 files changed, 116 insertions(+), 152 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index c00078499..0a84f30c4 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -41,8 +41,6 @@ namespace Emby.Dlna.Didl private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/"; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly DeviceProfile _profile; private readonly IImageProcessor _imageProcessor; private readonly string _serverAddress; @@ -317,7 +315,7 @@ namespace Emby.Dlna.Didl if (mediaSource.RunTimeTicks.HasValue) { - writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); + writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); } if (filter.Contains("res@size")) @@ -328,7 +326,7 @@ namespace Emby.Dlna.Didl if (size.HasValue) { - writer.WriteAttributeString("size", size.Value.ToString(_usCulture)); + writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture)); } } } @@ -342,7 +340,7 @@ namespace Emby.Dlna.Didl if (targetChannels.HasValue) { - writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); + writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture)); } if (filter.Contains("res@resolution")) @@ -361,12 +359,12 @@ namespace Emby.Dlna.Didl if (targetSampleRate.HasValue) { - writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); + writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture)); } if (totalBitrate.HasValue) { - writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture)); + writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture)); } var mediaProfile = _profile.GetVideoMediaProfile( @@ -552,7 +550,7 @@ namespace Emby.Dlna.Didl if (mediaSource.RunTimeTicks.HasValue) { - writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); + writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); } if (filter.Contains("res@size")) @@ -563,7 +561,7 @@ namespace Emby.Dlna.Didl if (size.HasValue) { - writer.WriteAttributeString("size", size.Value.ToString(_usCulture)); + writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture)); } } } @@ -575,17 +573,17 @@ namespace Emby.Dlna.Didl if (targetChannels.HasValue) { - writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); + writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture)); } if (targetSampleRate.HasValue) { - writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); + writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture)); } if (targetAudioBitrate.HasValue) { - writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture)); + writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture)); } var mediaProfile = _profile.GetAudioMediaProfile( @@ -639,7 +637,7 @@ namespace Emby.Dlna.Didl writer.WriteAttributeString("restricted", "1"); writer.WriteAttributeString("searchable", "1"); - writer.WriteAttributeString("childCount", childCount.ToString(_usCulture)); + writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture)); var clientId = GetClientId(folder, stubType); @@ -931,11 +929,11 @@ namespace Emby.Dlna.Didl if (item.IndexNumber.HasValue) { - AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp); + AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp); if (item is Episode) { - AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp); + AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp); } } } diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index b39bd5ce9..d17e23871 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -26,8 +26,6 @@ namespace Emby.Dlna.Eventing private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; @@ -83,7 +81,7 @@ namespace Emby.Dlna.Eventing if (!string.IsNullOrEmpty(header)) { // Starts with SECOND- - if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, _usCulture, out var val)) + if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { return val; } @@ -106,7 +104,7 @@ namespace Emby.Dlna.Eventing var response = new EventSubscriptionResponse(string.Empty, "text/plain"); response.Headers["SID"] = subscriptionId; - response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString; + response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString; return response; } @@ -163,7 +161,7 @@ namespace Emby.Dlna.Eventing options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType); options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange"); options.Headers.TryAddWithoutValidation("SID", subscription.Id); - options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture)); + options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture)); try { diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 11fcd81cf..0b2288000 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo { public class Device : IDisposable { - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; @@ -640,7 +638,7 @@ namespace Emby.Dlna.PlayTo return; } - Volume = int.Parse(volumeValue, UsCulture); + Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture); if (Volume > 0) { @@ -842,7 +840,7 @@ namespace Emby.Dlna.PlayTo if (!string.IsNullOrWhiteSpace(duration) && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) { - Duration = TimeSpan.Parse(duration, UsCulture); + Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture); } else { @@ -854,7 +852,7 @@ namespace Emby.Dlna.PlayTo if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) { - Position = TimeSpan.Parse(position, UsCulture); + Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture); } var track = result.Document.Descendants("TrackMetaData").FirstOrDefault(); @@ -1194,8 +1192,8 @@ namespace Emby.Dlna.PlayTo var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")); var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")); - var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture); - var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture); + var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture); + var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture); return new DeviceIcon { diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 0e49fd2c0..f25d8017e 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -30,8 +30,6 @@ namespace Emby.Dlna.PlayTo { public class PlayToController : ISessionController, IDisposable { - private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - private readonly SessionInfo _session; private readonly ISessionManager _sessionManager; private readonly ILibraryManager _libraryManager; @@ -716,7 +714,7 @@ namespace Emby.Dlna.PlayTo case GeneralCommandType.SetAudioStreamIndex: if (command.Arguments.TryGetValue("Index", out string index)) { - if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val)) + if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { return SetAudioStreamIndex(val); } @@ -728,7 +726,7 @@ namespace Emby.Dlna.PlayTo case GeneralCommandType.SetSubtitleStreamIndex: if (command.Arguments.TryGetValue("Index", out index)) { - if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val)) + if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { return SetSubtitleStreamIndex(val); } @@ -740,7 +738,7 @@ namespace Emby.Dlna.PlayTo case GeneralCommandType.SetVolume: if (command.Arguments.TryGetValue("Volume", out string vol)) { - if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume)) + if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume)) { return _device.SetVolume(volume, cancellationToken); } diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index 4b92fbff4..cade7b4c2 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50"; private const string FriendlyName = "Jellyfin"; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IHttpClientFactory _httpClientFactory; public SsdpHttpClient(IHttpClientFactory httpClientFactory) @@ -80,10 +78,10 @@ namespace Emby.Dlna.PlayTo { using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url); options.Headers.UserAgent.ParseAdd(USERAGENT); - options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture)); - options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">"); + options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture)); + options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">"); options.Headers.TryAddWithoutValidation("NT", "upnp:event"); - options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture)); + options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture)); using var response = await _httpClientFactory.CreateClient(NamedClient.Default) .SendAsync(options, HttpCompletionOption.ResponseHeadersRead) diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 09525aae4..80a45f2b2 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -15,7 +15,6 @@ namespace Emby.Dlna.Server { private readonly DeviceProfile _profile; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly string _serverUdn; private readonly string _serverAddress; private readonly string _serverName; @@ -193,10 +192,10 @@ namespace Emby.Dlna.Server .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty)) .Append(""); builder.Append("") - .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture))) + .Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture))) .Append(""); builder.Append("") - .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture))) + .Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture))) .Append(""); builder.Append("") .Append(SecurityElement.Escape(icon.Depth ?? string.Empty)) diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 8aaa1f7bb..ac6606d39 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -23,7 +23,6 @@ namespace Emby.Server.Implementations.MediaEncoder { public class EncodingManager : IEncodingManager { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IFileSystem _fileSystem; private readonly ILogger _logger; private readonly IMediaEncoder _encoder; @@ -193,7 +192,7 @@ namespace Emby.Server.Implementations.MediaEncoder private string GetChapterImagePath(Video video, long chapterPositionTicks) { - var filename = video.DateModified.Ticks.ToString(_usCulture) + "_" + chapterPositionTicks.ToString(_usCulture) + ".jpg"; + var filename = video.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg"; return Path.Combine(GetChapterImagesPath(video), filename); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index b1c860d61..86933074d 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -2007,7 +2007,7 @@ namespace Jellyfin.Api.Controllers Response.Headers.Add(HeaderNames.CacheControl, "public"); } - Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false))); + Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture)); // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 11f67ee89..1849dd047 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -376,7 +376,7 @@ namespace Jellyfin.Api.Controllers var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); var url = string.Format( - CultureInfo.CurrentCulture, + CultureInfo.InvariantCulture, "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", positionTicks.ToString(CultureInfo.InvariantCulture), endPositionTicks.ToString(CultureInfo.InvariantCulture), diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index 0853200dd..afdaf448b 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -57,9 +57,7 @@ namespace MediaBrowser.Controller.Entities public IList GetTaggedItems(InternalItemsQuery query) { - var usCulture = new CultureInfo("en-US"); - - if (!int.TryParse(Name, NumberStyles.Integer, usCulture, out var year)) + if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { return new List(); } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index bdb379332..5715194b8 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -21,8 +21,6 @@ namespace MediaBrowser.Controller.MediaEncoding { public class EncodingHelper { - private static readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IMediaEncoder _mediaEncoder; private readonly ISubtitleEncoder _subtitleEncoder; @@ -816,7 +814,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { - if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel)) + if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double requestLevel)) { if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) @@ -911,7 +909,7 @@ namespace MediaBrowser.Controller.MediaEncoding CultureInfo.InvariantCulture, "subtitles='{0}:si={1}'{2}", _mediaEncoder.EscapeSubtitleFilterPath(mediaPath), - state.InternalSubtitleStreamOffset.ToString(_usCulture), + state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture), // fallbackFontParam, setPtsParam); } @@ -1217,7 +1215,7 @@ namespace MediaBrowser.Controller.MediaEncoding param += string.Format( CultureInfo.InvariantCulture, " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", - profileScore.ToString(_usCulture), + profileScore.ToString(CultureInfo.InvariantCulture), crf, qmin, qmax); @@ -1289,7 +1287,7 @@ namespace MediaBrowser.Controller.MediaEncoding var framerate = GetFramerateParam(state); if (framerate.HasValue) { - param += string.Format(CultureInfo.InvariantCulture, " -r {0}", framerate.Value.ToString(_usCulture)); + param += string.Format(CultureInfo.InvariantCulture, " -r {0}", framerate.Value.ToString(CultureInfo.InvariantCulture)); } var targetVideoCodec = state.ActualOutputVideoCodec; @@ -1393,7 +1391,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) { // hevc_qsv use -level 51 instead of -level 153. - if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel)) + if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double hevcLevel)) { param += " -level " + (hevcLevel / 3); } @@ -1555,7 +1553,7 @@ namespace MediaBrowser.Controller.MediaEncoding // If a specific level was requested, the source must match or be less than var level = state.GetRequestedLevel(videoStream.Codec); if (!string.IsNullOrEmpty(level) - && double.TryParse(level, NumberStyles.Any, _usCulture, out var requestLevel)) + && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var requestLevel)) { if (!videoStream.Level.HasValue) { @@ -1803,7 +1801,7 @@ namespace MediaBrowser.Controller.MediaEncoding && state.AudioStream.Channels.Value > 5 && !encodingOptions.DownMixAudioBoost.Equals(1)) { - filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(_usCulture)); + filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; @@ -2434,8 +2432,8 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isExynosV4L2) { - var widthParam = requestedWidth.Value.ToString(_usCulture); - var heightParam = requestedHeight.Value.ToString(_usCulture); + var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture); + var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture); filters.Add( string.Format( @@ -2453,8 +2451,8 @@ namespace MediaBrowser.Controller.MediaEncoding // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size else if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue) { - var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture); - var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture); + var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture); + var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture); if (isExynosV4L2) { @@ -2486,7 +2484,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - var widthParam = requestedWidth.Value.ToString(_usCulture); + var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture); filters.Add( string.Format( @@ -2499,7 +2497,7 @@ namespace MediaBrowser.Controller.MediaEncoding // If a fixed height was requested else if (requestedHeight.HasValue) { - var heightParam = requestedHeight.Value.ToString(_usCulture); + var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture); if (isExynosV4L2) { @@ -2522,7 +2520,7 @@ namespace MediaBrowser.Controller.MediaEncoding // If a max width was requested else if (requestedMaxWidth.HasValue) { - var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture); + var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture); if (isExynosV4L2) { @@ -2545,7 +2543,7 @@ namespace MediaBrowser.Controller.MediaEncoding // If a max height was requested else if (requestedMaxHeight.HasValue) { - var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture); + var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture); if (isExynosV4L2) { @@ -4122,12 +4120,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (bitrate.HasValue) { - args += " -ab " + bitrate.Value.ToString(_usCulture); + args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioSampleRate.HasValue) { - args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture); + args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } args += GetAudioFilterParam(state, encodingOptions); @@ -4143,12 +4141,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (bitrate.HasValue) { - audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(_usCulture)); + audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture)); } if (state.OutputAudioChannels.HasValue) { - audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(_usCulture)); + audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); } // opus will fail on 44100 @@ -4156,7 +4154,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.OutputAudioSampleRate.HasValue) { - audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture)); + audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); } } diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index c4ddc5618..933f440ac 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -13,7 +13,6 @@ namespace MediaBrowser.Controller.MediaEncoding { public class JobLogger { - private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); private readonly ILogger _logger; public JobLogger(ILogger logger) @@ -87,7 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var rate = parts[i + 1]; - if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val)) + if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) { framerate = val; } @@ -96,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var rate = part.Split('=', 2)[^1]; - if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val)) + if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) { framerate = val; } @@ -106,7 +105,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var time = part.Split('=', 2)[^1]; - if (TimeSpan.TryParse(time, _usCulture, out var val)) + if (TimeSpan.TryParse(time, CultureInfo.InvariantCulture, out var val)) { var currentMs = startMs + val.TotalMilliseconds; @@ -128,7 +127,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (long.TryParse(size, NumberStyles.Any, _usCulture, out var val)) + if (long.TryParse(size, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) { bytesTranscoded = val * scale.Value; } @@ -147,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (scale.HasValue) { - if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val)) + if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val)) { bitRate = (int)Math.Ceiling(val * scale.Value); } diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index b7398880e..988581df9 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -60,8 +60,6 @@ namespace MediaBrowser.LocalMetadata.Images private readonly IFileSystem _fileSystem; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - /// /// Initializes a new instance of the class. /// @@ -434,7 +432,7 @@ namespace MediaBrowser.LocalMetadata.Images var seasonMarker = seasonNumber.Value == 0 ? "-specials" - : seasonNumber.Value.ToString("00", _usCulture); + : seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture); // Get this one directly from the file system since we have to go up a level if (!string.Equals(prefix, seasonMarker, StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 7c9e681d6..5a36c1663 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -21,8 +21,6 @@ namespace MediaBrowser.LocalMetadata.Parsers public class BaseItemXmlParser where T : BaseItem { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private Dictionary? _validProviderIds; /// @@ -180,7 +178,7 @@ namespace MediaBrowser.LocalMetadata.Parsers if (!string.IsNullOrEmpty(text)) { - if (float.TryParse(text, NumberStyles.Any, _usCulture, out var value)) + if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { item.CriticRating = value; } @@ -332,7 +330,7 @@ namespace MediaBrowser.LocalMetadata.Parsers if (!string.IsNullOrWhiteSpace(text)) { - if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, _usCulture, out var runtime)) + if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) { item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } @@ -1095,7 +1093,7 @@ namespace MediaBrowser.LocalMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var intVal)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal)) { sortOrder = intVal; } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index 6a3896eb6..6f66fd61b 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -25,8 +25,6 @@ namespace MediaBrowser.LocalMetadata.Savers /// public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; - private static readonly CultureInfo _usCulture = new CultureInfo("en-US"); - /// /// Initializes a new instance of the class. /// @@ -205,7 +203,7 @@ namespace MediaBrowser.LocalMetadata.Savers if (item.CriticRating.HasValue) { - writer.WriteElementString("CriticRating", item.CriticRating.Value.ToString(_usCulture)); + writer.WriteElementString("CriticRating", item.CriticRating.Value.ToString(CultureInfo.InvariantCulture)); } if (!string.IsNullOrEmpty(item.Overview)) @@ -289,12 +287,12 @@ namespace MediaBrowser.LocalMetadata.Savers if (item.CommunityRating.HasValue) { - writer.WriteElementString("Rating", item.CommunityRating.Value.ToString(_usCulture)); + writer.WriteElementString("Rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); } if (item.ProductionYear.HasValue && item is not Person) { - writer.WriteElementString("ProductionYear", item.ProductionYear.Value.ToString(_usCulture)); + writer.WriteElementString("ProductionYear", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)); } if (item is IHasAspectRatio hasAspectRatio) @@ -322,7 +320,7 @@ namespace MediaBrowser.LocalMetadata.Savers { var timespan = TimeSpan.FromTicks(runTimeTicks.Value); - writer.WriteElementString("RunningTime", Math.Floor(timespan.TotalMinutes).ToString(_usCulture)); + writer.WriteElementString("RunningTime", Math.Floor(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture)); } if (item.ProviderIds != null) @@ -395,7 +393,7 @@ namespace MediaBrowser.LocalMetadata.Savers if (person.SortOrder.HasValue) { - writer.WriteElementString("SortOrder", person.SortOrder.Value.ToString(_usCulture)); + writer.WriteElementString("SortOrder", person.SortOrder.Value.ToString(CultureInfo.InvariantCulture)); } writer.WriteEndElement(); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4cbd1bbc8..06fe95ce8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -43,11 +43,6 @@ namespace MediaBrowser.MediaEncoding.Encoder /// internal const int DefaultHdrImageExtractionTimeout = 20000; - /// - /// The us culture. - /// - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly ILogger _logger; private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; @@ -687,7 +682,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public string GetTimeParameter(TimeSpan time) { - return time.ToString(@"hh\:mm\:ss\.fff", _usCulture); + return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture); } public async Task ExtractVideoImagesOnInterval( @@ -704,11 +699,11 @@ namespace MediaBrowser.MediaEncoding.Encoder { var inputArgument = GetInputArgument(inputFile, mediaSource); - var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture); + var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture); if (maxWidth.HasValue) { - var maxWidthParam = maxWidth.Value.ToString(_usCulture); + var maxWidthParam = maxWidth.Value.ToString(CultureInfo.InvariantCulture); vf += string.Format(CultureInfo.InvariantCulture, ",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam); } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 26f629a31..9ed6c264e 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -30,7 +30,6 @@ namespace MediaBrowser.MediaEncoding.Probing private static readonly Regex _performerPattern = new (@"(?.*) \((?.*)\)"); - private readonly CultureInfo _usCulture = new ("en-US"); private readonly ILogger _logger; private readonly ILocalizationManager _localization; @@ -83,7 +82,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (!string.IsNullOrEmpty(data.Format.BitRate)) { - if (int.TryParse(data.Format.BitRate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(data.Format.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { info.Bitrate = value; } @@ -191,7 +190,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (data.Format != null && !string.IsNullOrEmpty(data.Format.Duration)) { - info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, _usCulture)).Ticks; + info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, CultureInfo.InvariantCulture)).Ticks; } FetchWtvInfo(info, data); @@ -673,7 +672,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (!string.IsNullOrEmpty(streamInfo.SampleRate)) { - if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { stream.SampleRate = value; } @@ -802,7 +801,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (!string.IsNullOrEmpty(streamInfo.BitRate)) { - if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { bitrate = value; } @@ -815,7 +814,7 @@ namespace MediaBrowser.MediaEncoding.Probing && (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio))) { // If the stream info doesn't have a bitrate get the value from the media format info - if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value)) + if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { bitrate = value; } @@ -921,8 +920,8 @@ namespace MediaBrowser.MediaEncoding.Probing var parts = (original ?? string.Empty).Split(':'); if (!(parts.Length == 2 && - int.TryParse(parts[0], NumberStyles.Any, _usCulture, out var width) && - int.TryParse(parts[1], NumberStyles.Any, _usCulture, out var height) && + int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width) && + int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height) && width > 0 && height > 0)) { @@ -1008,11 +1007,11 @@ namespace MediaBrowser.MediaEncoding.Probing if (parts.Length == 2) { - result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture); + result = float.Parse(parts[0], CultureInfo.InvariantCulture) / float.Parse(parts[1], CultureInfo.InvariantCulture); } else { - result = float.Parse(parts[0], _usCulture); + result = float.Parse(parts[0], CultureInfo.InvariantCulture); } return float.IsNaN(result) ? null : result; @@ -1039,7 +1038,7 @@ namespace MediaBrowser.MediaEncoding.Probing // If we got something, parse it if (!string.IsNullOrEmpty(duration)) { - data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks; + data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, CultureInfo.InvariantCulture)).Ticks; } } @@ -1101,7 +1100,7 @@ namespace MediaBrowser.MediaEncoding.Probing return; } - info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, _usCulture); + info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, CultureInfo.InvariantCulture); } private void SetAudioInfoFromTags(MediaInfo audio, IReadOnlyDictionary tags) @@ -1144,7 +1143,7 @@ namespace MediaBrowser.MediaEncoding.Probing { Name = match.Groups["name"].Value, Type = PersonType.Actor, - Role = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value) + Role = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value) }); } } @@ -1443,7 +1442,7 @@ namespace MediaBrowser.MediaEncoding.Probing .ToArray(); } - if (tags.TryGetValue("WM/OriginalReleaseTime", out var year) && int.TryParse(year, NumberStyles.Integer, _usCulture, out var parsedYear)) + if (tags.TryGetValue("WM/OriginalReleaseTime", out var year) && int.TryParse(year, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear)) { video.ProductionYear = parsedYear; } diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index b213e7aa0..406d32cde 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs @@ -56,10 +56,10 @@ namespace MediaBrowser.Model.Globalization IEnumerable GetLocalizationOptions(); /// - /// Returns the correct for the given language. + /// Returns the correct for the given language. /// /// The language. - /// The correct for the given language. + /// The correct for the given language. CultureDto? FindLanguageInfo(string language); } } diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 7d259a9d3..4b05edd67 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -29,8 +29,6 @@ namespace MediaBrowser.Providers.Manager /// public class ImageSaver { - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - /// /// The _config. /// @@ -377,7 +375,7 @@ namespace MediaBrowser.Providers.Manager var seasonMarker = season.IndexNumber.Value == 0 ? "-specials" - : season.IndexNumber.Value.ToString("00", UsCulture); + : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); var imageFilename = "season" + seasonMarker + "-landscape" + extension; @@ -400,7 +398,7 @@ namespace MediaBrowser.Providers.Manager var seasonMarker = season.IndexNumber.Value == 0 ? "-specials" - : season.IndexNumber.Value.ToString("00", UsCulture); + : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); var imageFilename = "season" + seasonMarker + "-banner" + extension; @@ -495,12 +493,12 @@ namespace MediaBrowser.Providers.Manager var filenames = images.Select(i => Path.GetFileNameWithoutExtension(i.Path)).ToList(); var current = 1; - while (filenames.Contains(numberedIndexPrefix + current.ToString(UsCulture), StringComparer.OrdinalIgnoreCase)) + while (filenames.Contains(numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { current++; } - return numberedIndexPrefix + current.ToString(UsCulture); + return numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture); } /// @@ -539,7 +537,7 @@ namespace MediaBrowser.Providers.Manager var seasonMarker = season.IndexNumber.Value == 0 ? "-specials" - : season.IndexNumber.Value.ToString("00", UsCulture); + : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); var imageFilename = "season" + seasonMarker + "-fanart" + extension; @@ -556,7 +554,7 @@ namespace MediaBrowser.Providers.Manager if (item.IsInMixedFolder) { - return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(UsCulture), extension) }; + return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(CultureInfo.InvariantCulture), extension) }; } var extraFanartFilename = GetBackdropSaveFilename(item.GetImages(ImageType.Backdrop), "fanart", "fanart", outputIndex); @@ -568,7 +566,7 @@ namespace MediaBrowser.Providers.Manager if (EnableExtraThumbsDuplication) { - list.Add(Path.Combine(item.ContainingFolderPath, "extrathumbs", "thumb" + outputIndex.ToString(UsCulture) + extension)); + list.Add(Path.Combine(item.ContainingFolderPath, "extrathumbs", "thumb" + outputIndex.ToString(CultureInfo.InvariantCulture) + extension)); } return list.ToArray(); @@ -582,7 +580,7 @@ namespace MediaBrowser.Providers.Manager var seasonMarker = season.IndexNumber.Value == 0 ? "-specials" - : season.IndexNumber.Value.ToString("00", UsCulture); + : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); var imageFilename = "season" + seasonMarker + "-poster" + extension; diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 479ae0f39..b2bc58eea 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -26,7 +26,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IHttpClientFactory _httpClientFactory; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IApplicationHost _appHost; private readonly JsonSerializerOptions _jsonOptions; @@ -79,7 +78,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb } if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4 - && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year) + && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var year) && year >= 0) { item.ProductionYear = year; @@ -93,14 +92,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb } if (!string.IsNullOrEmpty(result.imdbVotes) - && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount) + && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount) && voteCount >= 0) { // item.VoteCount = voteCount; } if (!string.IsNullOrEmpty(result.imdbRating) - && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating) + && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating) && imdbRating >= 0) { item.CommunityRating = imdbRating; @@ -191,7 +190,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb } if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4 - && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year) + && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var year) && year >= 0) { item.ProductionYear = year; @@ -205,14 +204,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb } if (!string.IsNullOrEmpty(result.imdbVotes) - && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount) + && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount) && voteCount >= 0) { // item.VoteCount = voteCount; } if (!string.IsNullOrEmpty(result.imdbRating) - && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating) + && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating) && imdbRating >= 0) { item.CommunityRating = imdbRating; diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index f7f4ea065..9d558b6ce 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -58,8 +58,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers _directoryService = directoryService; } - protected CultureInfo UsCulture { get; } = new CultureInfo("en-US"); - /// /// Gets the logger. /// @@ -309,7 +307,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrEmpty(text)) { - if (float.TryParse(text, NumberStyles.Any, UsCulture, out var value)) + if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { item.CriticRating = value; } @@ -370,7 +368,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers var val = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(val) && userData != null) { - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var count)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count)) { userData.PlayCount = count; } @@ -475,7 +473,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(text)) { - if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, UsCulture, out var runtime)) + if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) { item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } @@ -1265,7 +1263,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var intVal)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal)) { sortOrder = intVal; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index 3a305024e..d2f349ad7 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -163,7 +163,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) { item.AirsBeforeEpisodeNumber = rval; } @@ -179,7 +179,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) { item.AirsAfterSeasonNumber = rval; } @@ -195,7 +195,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) { item.AirsBeforeSeasonNumber = rval; } @@ -211,7 +211,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) { item.AirsBeforeSeasonNumber = rval; } @@ -227,7 +227,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { // int.TryParse is local aware, so it can be problematic, force us culture - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval)) + if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval)) { item.AirsBeforeEpisodeNumber = rval; } diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs index 62f80e81b..2cd3fdf02 100644 --- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs @@ -17,8 +17,6 @@ namespace MediaBrowser.XbmcMetadata.Savers /// public class EpisodeNfoSaver : BaseNfoSaver { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - /// /// Initializes a new instance of the class. /// @@ -60,17 +58,17 @@ namespace MediaBrowser.XbmcMetadata.Savers if (episode.IndexNumber.HasValue) { - writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(_usCulture)); + writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)); } if (episode.IndexNumberEnd.HasValue) { - writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(_usCulture)); + writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(CultureInfo.InvariantCulture)); } if (episode.ParentIndexNumber.HasValue) { - writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(_usCulture)); + writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture)); } if (episode.PremiereDate.HasValue) @@ -84,28 +82,28 @@ namespace MediaBrowser.XbmcMetadata.Savers { if (episode.AirsAfterSeasonNumber.HasValue && episode.AirsAfterSeasonNumber.Value != -1) { - writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(_usCulture)); + writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(CultureInfo.InvariantCulture)); } if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1) { - writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture)); + writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(CultureInfo.InvariantCulture)); } if (episode.AirsBeforeSeasonNumber.HasValue && episode.AirsBeforeSeasonNumber.Value != -1) { - writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(_usCulture)); + writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(CultureInfo.InvariantCulture)); } if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1) { - writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture)); + writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(CultureInfo.InvariantCulture)); } var specialSeason = episode.AiredSeasonNumber; if (specialSeason.HasValue && specialSeason.Value != -1) { - writer.WriteElementString("displayseason", specialSeason.Value.ToString(_usCulture)); + writer.WriteElementString("displayseason", specialSeason.Value.ToString(CultureInfo.InvariantCulture)); } } } -- cgit v1.2.3 From c26e6d89b43cfd81c5735ae1a2ecd47cc43fda90 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sun, 10 Oct 2021 19:48:11 +0300 Subject: Move FfmpegException to MediaBrowser.Common --- MediaBrowser.Common/FfmpegException.cs | 39 ++++++++++++++++++++++ MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 1 + MediaBrowser.MediaEncoding/FfmpegException.cs | 39 ---------------------- .../Subtitles/SubtitleEncoder.cs | 1 + 4 files changed, 41 insertions(+), 39 deletions(-) create mode 100644 MediaBrowser.Common/FfmpegException.cs delete mode 100644 MediaBrowser.MediaEncoding/FfmpegException.cs (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.Common/FfmpegException.cs b/MediaBrowser.Common/FfmpegException.cs new file mode 100644 index 000000000..be420196d --- /dev/null +++ b/MediaBrowser.Common/FfmpegException.cs @@ -0,0 +1,39 @@ +using System; + +namespace MediaBrowser.Common +{ + /// + /// Represents errors that occur during interaction with FFmpeg. + /// + public class FfmpegException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public FfmpegException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public FfmpegException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a + /// reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if + /// no inner exception is specified. + /// + public FfmpegException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 06fe95ce8..3dbbeda8a 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -12,6 +12,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Jellyfin.Extensions.Json; +using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; diff --git a/MediaBrowser.MediaEncoding/FfmpegException.cs b/MediaBrowser.MediaEncoding/FfmpegException.cs deleted file mode 100644 index 1697fd33a..000000000 --- a/MediaBrowser.MediaEncoding/FfmpegException.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; - -namespace MediaBrowser.MediaEncoding -{ - /// - /// Represents errors that occur during interaction with FFmpeg. - /// - public class FfmpegException : Exception - { - /// - /// Initializes a new instance of the class. - /// - public FfmpegException() - { - } - - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public FfmpegException(string message) : base(message) - { - } - - /// - /// Initializes a new instance of the class with a specified error message and a - /// reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// - /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if - /// no inner exception is specified. - /// - public FfmpegException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index bfc17904c..2b2de2ff6 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -11,6 +11,7 @@ using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; -- cgit v1.2.3 From 8d70cc2dde81aad0f484a2f0d0c5b90e6e1b97cd Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Thu, 7 Oct 2021 22:37:59 +0200 Subject: Add support for non-jpg image extractions --- .../MediaEncoding/IMediaEncoder.cs | 3 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 21 ++++---- .../Probing/ProbeResultNormalizer.cs | 18 +++---- .../MediaInfo/EmbeddedImageProvider.cs | 60 ++++++++++++++++------ 4 files changed, 62 insertions(+), 40 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index c5522bc3c..638588560 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -95,9 +95,10 @@ namespace MediaBrowser.Controller.MediaEncoding /// Media source information. /// Media stream information. /// Index of the stream to extract from. + /// The extension of the file to write. /// CancellationToken to use for operation. /// Location of video image. - Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken); + Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken); /// /// Extracts the video images on interval. diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 06fe95ce8..30bc7125d 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -468,17 +468,17 @@ namespace MediaBrowser.MediaEncoding.Encoder Protocol = MediaProtocol.File }; - return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken); + return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, "jpg", cancellationToken); } public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { - return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, cancellationToken); + return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, "jpg", cancellationToken); } - public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken) + public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken) { - return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken); + return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, outputExtension, cancellationToken); } private async Task ExtractImage( @@ -490,6 +490,7 @@ namespace MediaBrowser.MediaEncoding.Encoder bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, + string outputExtension, CancellationToken cancellationToken) { var inputArgument = GetInputArgument(inputFile, mediaSource); @@ -499,7 +500,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter. try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, outputExtension, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -512,7 +513,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, outputExtension, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -525,7 +526,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, outputExtension, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -537,17 +538,17 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, outputExtension, cancellationToken).ConfigureAwait(false); } - private async Task ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, CancellationToken cancellationToken) + private async Task ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, string outputExtension, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) { throw new ArgumentNullException(nameof(inputPath)); } - var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); + var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + "." + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar. diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 5f6539495..775689095 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -582,7 +582,8 @@ namespace MediaBrowser.MediaEncoding.Probing /// MediaAttachments. private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo) { - if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase) && + !(streamInfo.Disposition != null && streamInfo.Disposition.GetValueOrDefault("attached_pic") == 1)) { return null; } @@ -735,15 +736,14 @@ namespace MediaBrowser.MediaEncoding.Probing else if (string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)) { // How to differentiate between video and embedded image? - // check disposition, alternately: presence of codec tag, also embedded images have high (unusual) framerates - if ((streamInfo.Disposition != null && streamInfo.Disposition.GetValueOrDefault("attached_pic") == 1) || - string.IsNullOrWhiteSpace(stream.CodecTag)) + // The only difference I've seen thus far is presence of codec tag, also embedded images have high (unusual) framerates + if (!string.IsNullOrWhiteSpace(stream.CodecTag)) { - stream.Type = MediaStreamType.EmbeddedImage; + stream.Type = MediaStreamType.Video; } else { - stream.Type = MediaStreamType.Video; + stream.Type = MediaStreamType.EmbeddedImage; } } else @@ -812,12 +812,6 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.ColorPrimaries = streamInfo.ColorPrimaries; } - - // workaround for mkv attached_pics losing filename due to being classified as video based on codec - if (stream.Type == MediaStreamType.EmbeddedImage && streamInfo.Tags != null && string.IsNullOrEmpty(stream.Comment)) - { - stream.Comment = GetDictionaryValue(streamInfo.Tags, "filename"); - } } else { diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index 11fdfcdf9..ad95cdb06 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,6 +16,7 @@ using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo @@ -115,25 +117,49 @@ namespace MediaBrowser.Providers.MediaInfo Protocol = item.PathProtocol ?? MediaProtocol.File, }; - string[] imageFileNames; - switch (type) + string[] imageFileNames = type switch { - case ImageType.Backdrop: - imageFileNames = _backdropImageFileNames; - break; - case ImageType.Logo: - imageFileNames = _logoImageFileNames; - break; - case ImageType.Primary: - default: - imageFileNames = _primaryImageFileNames; - break; + ImageType.Primary => _primaryImageFileNames, + ImageType.Backdrop => _backdropImageFileNames, + ImageType.Logo => _logoImageFileNames, + _ => _primaryImageFileNames + }; + + // Try attachments first + var attachmentSources = item.GetMediaSources(false).SelectMany(source => source.MediaAttachments).ToList(); + var attachmentStream = attachmentSources + .Where(stream => !string.IsNullOrEmpty(stream.FileName)) + .First(stream => imageFileNames.Any(name => stream.FileName.Contains(name, StringComparison.OrdinalIgnoreCase))); + + if (attachmentStream != null) + { + var extension = (string.IsNullOrEmpty(attachmentStream.MimeType) ? + Path.GetExtension(attachmentStream.FileName) : + MimeTypes.ToExtension(attachmentStream.MimeType)) ?? "jpg"; + + string extractedAttachmentPath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, extension, cancellationToken).ConfigureAwait(false); + + ImageFormat format = extension switch + { + "bmp" => ImageFormat.Bmp, + "gif" => ImageFormat.Gif, + "jpg" => ImageFormat.Jpg, + "png" => ImageFormat.Png, + "webp" => ImageFormat.Webp, + _ => ImageFormat.Jpg + }; + + return new DynamicImageResponse + { + Format = format, + HasImage = true, + Path = extractedAttachmentPath, + Protocol = MediaProtocol.File + }; } - var imageStreams = - item.GetMediaStreams() - .Where(i => i.Type == MediaStreamType.EmbeddedImage) - .ToList(); + // Fall back to EmbeddedImage streams + var imageStreams = item.GetMediaStreams().FindAll(i => i.Type == MediaStreamType.EmbeddedImage); if (!imageStreams.Any()) { @@ -160,7 +186,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - string extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, cancellationToken).ConfigureAwait(false); + string extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, "jpg", cancellationToken).ConfigureAwait(false); return new DynamicImageResponse { -- cgit v1.2.3 From e3eee10d05e9ecc7e3fac1f8fdad92329d38a4db Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Mon, 11 Oct 2021 12:34:18 +0200 Subject: Add image provider tests and clean up --- .../MediaEncoding/IMediaEncoder.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 +- .../MediaInfo/EmbeddedImageProvider.cs | 41 ++-- .../MediaInfo/VideoImageProvider.cs | 9 +- .../MediaInfo/EmbeddedImageProviderTests.cs | 211 +++++++++++++++++++++ .../MediaInfo/VideoImageProviderTests.cs | 168 ++++++++++++++++ 6 files changed, 408 insertions(+), 29 deletions(-) create mode 100644 tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs create mode 100644 tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 638588560..e6511ca8d 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// Media source information. /// Media stream information. /// Index of the stream to extract from. - /// The extension of the file to write. + /// The extension of the file to write, including the '.'. /// CancellationToken to use for operation. /// Location of video image. Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 30bc7125d..dac2c6a26 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -468,12 +468,12 @@ namespace MediaBrowser.MediaEncoding.Encoder Protocol = MediaProtocol.File }; - return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, "jpg", cancellationToken); + return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ".jpg", cancellationToken); } public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { - return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, "jpg", cancellationToken); + return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ".jpg", cancellationToken); } public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken) @@ -548,7 +548,7 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new ArgumentNullException(nameof(inputPath)); } - var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + "." + outputExtension); + var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar. diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index ad95cdb06..df87f2d49 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -1,9 +1,7 @@ -#nullable enable #pragma warning disable CS1591 using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading; @@ -17,7 +15,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo { @@ -48,12 +45,10 @@ namespace MediaBrowser.Providers.MediaInfo }; private readonly IMediaEncoder _mediaEncoder; - private readonly ILogger _logger; - public EmbeddedImageProvider(IMediaEncoder mediaEncoder, ILogger logger) + public EmbeddedImageProvider(IMediaEncoder mediaEncoder) { _mediaEncoder = mediaEncoder; - _logger = logger; } /// @@ -84,7 +79,7 @@ namespace MediaBrowser.Providers.MediaInfo }; } - return ImmutableList.Empty; + return new List(); } /// @@ -98,13 +93,6 @@ namespace MediaBrowser.Providers.MediaInfo return Task.FromResult(new DynamicImageResponse { HasImage = false }); } - // Can't extract if we didn't find any video streams in the file - if (!video.DefaultVideoStreamIndex.HasValue) - { - _logger.LogInformation("Skipping image extraction due to missing DefaultVideoStreamIndex for {Path}.", video.Path ?? string.Empty); - return Task.FromResult(new DynamicImageResponse { HasImage = false }); - } - return GetEmbeddedImage(video, type, cancellationToken); } @@ -128,24 +116,29 @@ namespace MediaBrowser.Providers.MediaInfo // Try attachments first var attachmentSources = item.GetMediaSources(false).SelectMany(source => source.MediaAttachments).ToList(); var attachmentStream = attachmentSources - .Where(stream => !string.IsNullOrEmpty(stream.FileName)) - .First(stream => imageFileNames.Any(name => stream.FileName.Contains(name, StringComparison.OrdinalIgnoreCase))); + .Where(attachment => !string.IsNullOrEmpty(attachment.FileName)) + .FirstOrDefault(attachment => imageFileNames.Any(name => attachment.FileName.Contains(name, StringComparison.OrdinalIgnoreCase))); if (attachmentStream != null) { - var extension = (string.IsNullOrEmpty(attachmentStream.MimeType) ? + var extension = string.IsNullOrEmpty(attachmentStream.MimeType) ? Path.GetExtension(attachmentStream.FileName) : - MimeTypes.ToExtension(attachmentStream.MimeType)) ?? "jpg"; + MimeTypes.ToExtension(attachmentStream.MimeType); + + if (string.IsNullOrEmpty(extension)) + { + extension = ".jpg"; + } string extractedAttachmentPath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, extension, cancellationToken).ConfigureAwait(false); ImageFormat format = extension switch { - "bmp" => ImageFormat.Bmp, - "gif" => ImageFormat.Gif, - "jpg" => ImageFormat.Jpg, - "png" => ImageFormat.Png, - "webp" => ImageFormat.Webp, + ".bmp" => ImageFormat.Bmp, + ".gif" => ImageFormat.Gif, + ".jpg" => ImageFormat.Jpg, + ".png" => ImageFormat.Png, + ".webp" => ImageFormat.Webp, _ => ImageFormat.Jpg }; @@ -170,7 +163,7 @@ namespace MediaBrowser.Providers.MediaInfo // Extract first stream containing an element of imageFileNames var imageStream = imageStreams .Where(stream => !string.IsNullOrEmpty(stream.Comment)) - .First(stream => imageFileNames.Any(name => stream.Comment.Contains(name, StringComparison.OrdinalIgnoreCase))); + .FirstOrDefault(stream => imageFileNames.Any(name => stream.Comment.Contains(name, StringComparison.OrdinalIgnoreCase))); // Primary type only: default to first image if none found by label if (imageStream == null) diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index 8f2009950..60739f156 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -81,7 +81,14 @@ namespace MediaBrowser.Providers.MediaInfo ? TimeSpan.FromTicks(item.RunTimeTicks.Value / 10) : TimeSpan.FromSeconds(10); - var videoStream = item.GetMediaStreams().FirstOrDefault(i => i.Type == MediaStreamType.Video); + var videoStream = item.GetDefaultVideoStream() ?? item.GetMediaStreams().FirstOrDefault(i => i.Type == MediaStreamType.Video); + + if (videoStream == null) + { + _logger.LogInformation("Skipping image extraction: no video stream found for {Path}.", item.Path ?? string.Empty); + return new DynamicImageResponse { HasImage = false }; + } + string extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false); return new DynamicImageResponse diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs new file mode 100644 index 000000000..fcea1532a --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs @@ -0,0 +1,211 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.MediaInfo; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.MediaInfo +{ + public class EmbeddedImageProviderTests + { + public static TheoryData GetSupportedImages_Empty_TestData => + new () + { + new AudioBook(), + new BoxSet(), + new Series(), + new Season(), + }; + + public static TheoryData> GetSupportedImages_Populated_TestData => + new TheoryData> + { + { new Episode(), new List { ImageType.Primary } }, + { new Movie(), new List { ImageType.Logo, ImageType.Backdrop, ImageType.Primary } }, + }; + + private EmbeddedImageProvider GetEmbeddedImageProvider(IMediaEncoder? mediaEncoder) + { + return new EmbeddedImageProvider(mediaEncoder); + } + + [Theory] + [MemberData(nameof(GetSupportedImages_Empty_TestData))] + public void GetSupportedImages_Empty(BaseItem item) + { + var embeddedImageProvider = GetEmbeddedImageProvider(null); + Assert.False(embeddedImageProvider.GetSupportedImages(item).Any()); + } + + [Theory] + [MemberData(nameof(GetSupportedImages_Populated_TestData))] + public void GetSupportedImages_Populated(BaseItem item, IEnumerable expected) + { + var embeddedImageProvider = GetEmbeddedImageProvider(null); + var actual = embeddedImageProvider.GetSupportedImages(item); + Assert.Equal(expected.OrderBy(i => i.ToString()), actual.OrderBy(i => i.ToString())); + } + + [Fact] + public async void GetImage_Empty_NoStreams() + { + var embeddedImageProvider = GetEmbeddedImageProvider(null); + + var input = new Mock(); + input.Setup(movie => movie.GetMediaSources(It.IsAny())) + .Returns(new List()); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List()); + + var actual = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actual); + Assert.False(actual.HasImage); + } + + [Fact] + public async void GetImage_Empty_NoLabeledAttachments() + { + var embeddedImageProvider = GetEmbeddedImageProvider(null); + + var input = new Mock(); + // add an attachment without a filename - has a list to look through but finds nothing + input.Setup(movie => movie.GetMediaSources(It.IsAny())) + .Returns(new List { new () { MediaAttachments = new List { new () } } }); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List()); + + var actual = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actual); + Assert.False(actual.HasImage); + } + + [Fact] + public async void GetImage_Empty_NoEmbeddedLabeledBackdrop() + { + var embeddedImageProvider = GetEmbeddedImageProvider(null); + + var input = new Mock(); + input.Setup(movie => movie.GetMediaSources(It.IsAny())) + .Returns(new List()); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List { new () { Type = MediaStreamType.EmbeddedImage } }); + + var actual = await embeddedImageProvider.GetImage(input.Object, ImageType.Backdrop, CancellationToken.None); + Assert.NotNull(actual); + Assert.False(actual.HasImage); + } + + [Fact] + public async void GetImage_Attached() + { + // first tests file extension detection, second uses mimetype, third defaults to jpg + MediaAttachment sampleAttachment1 = new () { FileName = "clearlogo.png", Index = 1 }; + MediaAttachment sampleAttachment2 = new () { FileName = "backdrop", MimeType = "image/bmp", Index = 2 }; + MediaAttachment sampleAttachment3 = new () { FileName = "poster", Index = 3 }; + string targetPath1 = "path1.png"; + string targetPath2 = "path2.bmp"; + string targetPath3 = "path2.jpg"; + + var mediaEncoder = new Mock(MockBehavior.Strict); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), 1, ".png", CancellationToken.None)) + .Returns(Task.FromResult(targetPath1)); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), 2, ".bmp", CancellationToken.None)) + .Returns(Task.FromResult(targetPath2)); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), 3, ".jpg", CancellationToken.None)) + .Returns(Task.FromResult(targetPath3)); + var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object); + + var input = new Mock(); + input.Setup(movie => movie.GetMediaSources(It.IsAny())) + .Returns(new List { new () { MediaAttachments = new List { sampleAttachment1, sampleAttachment2, sampleAttachment3 } } }); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List()); + + var actualLogo = await embeddedImageProvider.GetImage(input.Object, ImageType.Logo, CancellationToken.None); + Assert.NotNull(actualLogo); + Assert.True(actualLogo.HasImage); + Assert.Equal(targetPath1, actualLogo.Path); + Assert.Equal(ImageFormat.Png, actualLogo.Format); + + var actualBackdrop = await embeddedImageProvider.GetImage(input.Object, ImageType.Backdrop, CancellationToken.None); + Assert.NotNull(actualBackdrop); + Assert.True(actualBackdrop.HasImage); + Assert.Equal(targetPath2, actualBackdrop.Path); + Assert.Equal(ImageFormat.Bmp, actualBackdrop.Format); + + var actualPrimary = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actualPrimary); + Assert.True(actualPrimary.HasImage); + Assert.Equal(targetPath3, actualPrimary.Path); + Assert.Equal(ImageFormat.Jpg, actualPrimary.Format); + } + + [Fact] + public async void GetImage_EmbeddedDefault() + { + MediaStream sampleStream = new () { Type = MediaStreamType.EmbeddedImage, Index = 1 }; + string targetPath = "path"; + + var mediaEncoder = new Mock(MockBehavior.Strict); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), sampleStream, 1, "jpg", CancellationToken.None)) + .Returns(Task.FromResult(targetPath)); + var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object); + + var input = new Mock(); + input.Setup(movie => movie.GetMediaSources(It.IsAny())) + .Returns(new List()); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List() { sampleStream }); + + var actual = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actual); + Assert.True(actual.HasImage); + Assert.Equal(targetPath, actual.Path); + Assert.Equal(ImageFormat.Jpg, actual.Format); + } + + [Fact] + public async void GetImage_EmbeddedSelection() + { + // primary is second stream to ensure it's not defaulting, backdrop is first + MediaStream sampleStream1 = new () { Type = MediaStreamType.EmbeddedImage, Index = 1, Comment = "backdrop" }; + MediaStream sampleStream2 = new () { Type = MediaStreamType.EmbeddedImage, Index = 2, Comment = "cover" }; + string targetPath1 = "path1.jpg"; + string targetPath2 = "path2.jpg"; + + var mediaEncoder = new Mock(MockBehavior.Strict); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), sampleStream1, 1, "jpg", CancellationToken.None)) + .Returns(Task.FromResult(targetPath1)); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), sampleStream2, 2, "jpg", CancellationToken.None)) + .Returns(Task.FromResult(targetPath2)); + var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object); + + var input = new Mock(); + input.Setup(movie => movie.GetMediaSources(It.IsAny())) + .Returns(new List()); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List { sampleStream1, sampleStream2 }); + + var actualPrimary = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actualPrimary); + Assert.True(actualPrimary.HasImage); + Assert.Equal(targetPath2, actualPrimary.Path); + Assert.Equal(ImageFormat.Jpg, actualPrimary.Format); + + var actualBackdrop = await embeddedImageProvider.GetImage(input.Object, ImageType.Backdrop, CancellationToken.None); + Assert.NotNull(actualBackdrop); + Assert.True(actualBackdrop.HasImage); + Assert.Equal(targetPath1, actualBackdrop.Path); + Assert.Equal(ImageFormat.Jpg, actualBackdrop.Format); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs new file mode 100644 index 000000000..9a5cd79bb --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.MediaInfo; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.MediaInfo +{ + public class VideoImageProviderTests + { + private VideoImageProvider GetVideoImageProvider(IMediaEncoder? mediaEncoder) + { + // strict to ensure this isn't accidentally used where a prepared mock is intended + mediaEncoder ??= new Mock(MockBehavior.Strict).Object; + return new VideoImageProvider(mediaEncoder, new NullLogger()); + } + + [Fact] + public async void GetImage_Empty_IsPlaceholder() + { + var videoImageProvider = GetVideoImageProvider(null); + + var input = new Mock(); + input.Object.IsPlaceHolder = true; + + var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actual); + Assert.False(actual.HasImage); + } + + [Fact] + public async void GetImage_Empty_NoDefaultVideoStream() + { + var videoImageProvider = GetVideoImageProvider(null); + + var input = new Mock(); + + var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actual); + Assert.False(actual.HasImage); + } + + [Fact] + public async void GetImage_Empty_DefaultSet_NoVideoStream() + { + var videoImageProvider = GetVideoImageProvider(null); + + var input = new Mock(); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List()); + // set a default index but don't put anything there (invalid input, but provider shouldn't break) + input.Object.DefaultVideoStreamIndex = 1; + + var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actual); + Assert.False(actual.HasImage); + } + + [Fact] + public async void GetImage_Extract_DefaultStream() + { + MediaStream firstStream = new () { Type = MediaStreamType.Video, Index = 0 }; + MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 1 }; + string targetPath = "path.jpg"; + + var mediaEncoder = new Mock(MockBehavior.Strict); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), firstStream, It.IsAny(), It.IsAny(), CancellationToken.None)) + .Returns(Task.FromResult("wrong stream called!")); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), targetStream, It.IsAny(), It.IsAny(), CancellationToken.None)) + .Returns(Task.FromResult(targetPath)); + var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object); + + var input = new Mock(); + input.Setup(movie => movie.GetDefaultVideoStream()) + .Returns(targetStream); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List() { firstStream, targetStream }); + input.Object.DefaultVideoStreamIndex = 1; + + var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actual); + Assert.True(actual.HasImage); + Assert.Equal(targetPath, actual.Path); + Assert.Equal(ImageFormat.Jpg, actual.Format); + } + + [Fact] + public async void GetImage_Extract_FallbackToFirstVideoStream() + { + MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 }; + string targetPath = "path.jpg"; + + var mediaEncoder = new Mock(MockBehavior.Strict); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), targetStream, It.IsAny(), It.IsAny(), CancellationToken.None)) + .Returns(Task.FromResult(targetPath)); + var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object); + + var input = new Mock(); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List() { targetStream }); + // default must be set, ensure a stream is still found if not pointed at a video + input.Object.DefaultVideoStreamIndex = 5; + + var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + Assert.NotNull(actual); + Assert.True(actual.HasImage); + Assert.Equal(targetPath, actual.Path); + Assert.Equal(ImageFormat.Jpg, actual.Format); + } + + [Fact] + public async void GetImage_Time_Default() + { + MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 }; + + TimeSpan? actualTimeSpan = null; + var mediaEncoder = new Mock(MockBehavior.Strict); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), CancellationToken.None)) + .Callback((_, _, _, _, _, timeSpan, _) => actualTimeSpan = timeSpan) + .Returns(Task.FromResult("path")); + var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object); + + var input = new Mock(); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List() { targetStream }); + // default must be set + input.Object.DefaultVideoStreamIndex = 0; + + // not testing return, just verifying what gets requested for time span + await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + + Assert.Equal(TimeSpan.FromSeconds(10), actualTimeSpan); + } + + [Fact] + public async void GetImage_Time_Calculated() + { + MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 }; + + TimeSpan? actualTimeSpan = null; + var mediaEncoder = new Mock(MockBehavior.Strict); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), CancellationToken.None)) + .Callback((_, _, _, _, _, timeSpan, _) => actualTimeSpan = timeSpan) + .Returns(Task.FromResult("path")); + var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object); + + var input = new Mock(); + input.Setup(movie => movie.GetMediaStreams()) + .Returns(new List() { targetStream }); + // default must be set + input.Object.DefaultVideoStreamIndex = 0; + input.Object.RunTimeTicks = 5000; + + // not testing return, just verifying what gets requested for time span + await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + + Assert.Equal(TimeSpan.FromTicks(500), actualTimeSpan); + } + } +} -- cgit v1.2.3 From 31baea072a1ed25346437150f27a93916cb4cca0 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Mon, 11 Oct 2021 21:25:12 +0200 Subject: Address review comments Clean up style Fix references in class summaries Combine Where+FirstOrDefault queries Break up large method, long lines Add validation on file extension Apply test naming conventions Extract mock of Movie class, comment on why not mocking interface Co-authored-by: Cody Robibero Co-authored-by: Claus Vium --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 9 ++ .../Probing/ProbeResultNormalizer.cs | 4 +- .../MediaInfo/EmbeddedImageProvider.cs | 95 +++++++------ .../MediaInfo/VideoImageProvider.cs | 10 +- .../MediaInfo/EmbeddedImageProviderTests.cs | 155 +++++++++++---------- .../MediaInfo/VideoImageProviderTests.cs | 108 +++++++------- 6 files changed, 208 insertions(+), 173 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index dac2c6a26..102b4f943 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -548,6 +548,15 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new ArgumentNullException(nameof(inputPath)); } + if (string.IsNullOrEmpty(outputExtension)) + { + outputExtension = ".jpg"; + } + else if (outputExtension[0] != '.') + { + outputExtension = "." + outputExtension; + } + var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 775689095..9279cb220 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -582,8 +582,8 @@ namespace MediaBrowser.MediaEncoding.Probing /// MediaAttachments. private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo) { - if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase) && - !(streamInfo.Disposition != null && streamInfo.Disposition.GetValueOrDefault("attached_pic") == 1)) + if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase) + && streamInfo.Disposition?.GetValueOrDefault("attached_pic") != 1) { return null; } diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index df87f2d49..1d9d1e02a 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.IO; @@ -19,7 +17,7 @@ using MediaBrowser.Model.Net; namespace MediaBrowser.Providers.MediaInfo { /// - /// Uses ffmpeg to extract embedded images. + /// Uses to extract embedded images. /// public class EmbeddedImageProvider : IDynamicImageProvider, IHasOrder { @@ -46,6 +44,10 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IMediaEncoder _mediaEncoder; + /// + /// Initializes a new instance of the class. + /// + /// The media encoder for extracting attached/embedded images. public EmbeddedImageProvider(IMediaEncoder mediaEncoder) { _mediaEncoder = mediaEncoder; @@ -65,13 +67,13 @@ namespace MediaBrowser.Providers.MediaInfo { if (item is Episode) { - return new List + return new[] { ImageType.Primary, }; } - return new List + return new[] { ImageType.Primary, ImageType.Backdrop, @@ -79,7 +81,7 @@ namespace MediaBrowser.Providers.MediaInfo }; } - return new List(); + return Array.Empty(); } /// @@ -114,47 +116,20 @@ namespace MediaBrowser.Providers.MediaInfo }; // Try attachments first - var attachmentSources = item.GetMediaSources(false).SelectMany(source => source.MediaAttachments).ToList(); - var attachmentStream = attachmentSources - .Where(attachment => !string.IsNullOrEmpty(attachment.FileName)) - .FirstOrDefault(attachment => imageFileNames.Any(name => attachment.FileName.Contains(name, StringComparison.OrdinalIgnoreCase))); + var attachmentStream = item.GetMediaSources(false) + .SelectMany(source => source.MediaAttachments) + .FirstOrDefault(attachment => !string.IsNullOrEmpty(attachment.FileName) + && imageFileNames.Any(name => attachment.FileName.Contains(name, StringComparison.OrdinalIgnoreCase))); if (attachmentStream != null) { - var extension = string.IsNullOrEmpty(attachmentStream.MimeType) ? - Path.GetExtension(attachmentStream.FileName) : - MimeTypes.ToExtension(attachmentStream.MimeType); - - if (string.IsNullOrEmpty(extension)) - { - extension = ".jpg"; - } - - string extractedAttachmentPath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, extension, cancellationToken).ConfigureAwait(false); - - ImageFormat format = extension switch - { - ".bmp" => ImageFormat.Bmp, - ".gif" => ImageFormat.Gif, - ".jpg" => ImageFormat.Jpg, - ".png" => ImageFormat.Png, - ".webp" => ImageFormat.Webp, - _ => ImageFormat.Jpg - }; - - return new DynamicImageResponse - { - Format = format, - HasImage = true, - Path = extractedAttachmentPath, - Protocol = MediaProtocol.File - }; + return await ExtractAttachment(item, cancellationToken, attachmentStream, mediaSource); } // Fall back to EmbeddedImage streams var imageStreams = item.GetMediaStreams().FindAll(i => i.Type == MediaStreamType.EmbeddedImage); - if (!imageStreams.Any()) + if (imageStreams.Count == 0) { // Can't extract if we don't have any EmbeddedImage streams return new DynamicImageResponse { HasImage = false }; @@ -162,8 +137,8 @@ namespace MediaBrowser.Providers.MediaInfo // Extract first stream containing an element of imageFileNames var imageStream = imageStreams - .Where(stream => !string.IsNullOrEmpty(stream.Comment)) - .FirstOrDefault(stream => imageFileNames.Any(name => stream.Comment.Contains(name, StringComparison.OrdinalIgnoreCase))); + .FirstOrDefault(stream => !string.IsNullOrEmpty(stream.Comment) + && imageFileNames.Any(name => stream.Comment.Contains(name, StringComparison.OrdinalIgnoreCase))); // Primary type only: default to first image if none found by label if (imageStream == null) @@ -179,7 +154,9 @@ namespace MediaBrowser.Providers.MediaInfo } } - string extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, "jpg", cancellationToken).ConfigureAwait(false); + string extractedImagePath = + await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, ".jpg", cancellationToken) + .ConfigureAwait(false); return new DynamicImageResponse { @@ -190,6 +167,40 @@ namespace MediaBrowser.Providers.MediaInfo }; } + private async Task ExtractAttachment(Video item, CancellationToken cancellationToken, MediaAttachment attachmentStream, MediaSourceInfo mediaSource) + { + var extension = string.IsNullOrEmpty(attachmentStream.MimeType) + ? Path.GetExtension(attachmentStream.FileName) + : MimeTypes.ToExtension(attachmentStream.MimeType); + + if (string.IsNullOrEmpty(extension)) + { + extension = ".jpg"; + } + + string extractedAttachmentPath = + await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, extension, cancellationToken) + .ConfigureAwait(false); + + ImageFormat format = extension switch + { + ".bmp" => ImageFormat.Bmp, + ".gif" => ImageFormat.Gif, + ".jpg" => ImageFormat.Jpg, + ".png" => ImageFormat.Png, + ".webp" => ImageFormat.Webp, + _ => ImageFormat.Jpg + }; + + return new DynamicImageResponse + { + Format = format, + HasImage = true, + Path = extractedAttachmentPath, + Protocol = MediaProtocol.File + }; + } + /// public bool Supports(BaseItem item) { diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index 60739f156..d226182c0 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -1,6 +1,3 @@ -#nullable enable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Linq; @@ -18,13 +15,18 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo { /// - /// Uses ffmpeg to create still images from the main video. + /// Uses to create still images from the main video. /// public class VideoImageProvider : IDynamicImageProvider, IHasOrder { private readonly IMediaEncoder _mediaEncoder; private readonly ILogger _logger; + /// + /// Initializes a new instance of the class. + /// + /// The media encoder for capturing images. + /// The logger. public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger logger) { _mediaEncoder = mediaEncoder; diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs index fcea1532a..b194e3885 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs @@ -17,38 +17,37 @@ namespace Jellyfin.Providers.Tests.MediaInfo { public class EmbeddedImageProviderTests { - public static TheoryData GetSupportedImages_Empty_TestData => - new () + private static TheoryData GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData() + { + return new () { new AudioBook(), new BoxSet(), new Series(), new Season(), }; - - public static TheoryData> GetSupportedImages_Populated_TestData => - new TheoryData> - { - { new Episode(), new List { ImageType.Primary } }, - { new Movie(), new List { ImageType.Logo, ImageType.Backdrop, ImageType.Primary } }, - }; - - private EmbeddedImageProvider GetEmbeddedImageProvider(IMediaEncoder? mediaEncoder) - { - return new EmbeddedImageProvider(mediaEncoder); } [Theory] - [MemberData(nameof(GetSupportedImages_Empty_TestData))] - public void GetSupportedImages_Empty(BaseItem item) + [MemberData(nameof(GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData))] + public void GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty(BaseItem item) { var embeddedImageProvider = GetEmbeddedImageProvider(null); - Assert.False(embeddedImageProvider.GetSupportedImages(item).Any()); + Assert.Empty(embeddedImageProvider.GetSupportedImages(item)); + } + + private static TheoryData> GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData() + { + return new TheoryData> + { + { new Episode(), new List { ImageType.Primary } }, + { new Movie(), new List { ImageType.Logo, ImageType.Backdrop, ImageType.Primary } }, + }; } [Theory] - [MemberData(nameof(GetSupportedImages_Populated_TestData))] - public void GetSupportedImages_Populated(BaseItem item, IEnumerable expected) + [MemberData(nameof(GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData))] + public void GetSupportedImages_SupportedBaseItems_ReturnsPopulated(BaseItem item, IEnumerable expected) { var embeddedImageProvider = GetEmbeddedImageProvider(null); var actual = embeddedImageProvider.GetSupportedImages(item); @@ -56,56 +55,34 @@ namespace Jellyfin.Providers.Tests.MediaInfo } [Fact] - public async void GetImage_Empty_NoStreams() + public async void GetImage_InputWithNoStreams_ReturnsNoImage() { var embeddedImageProvider = GetEmbeddedImageProvider(null); - var input = new Mock(); - input.Setup(movie => movie.GetMediaSources(It.IsAny())) - .Returns(new List()); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List()); + var input = GetMovie(new List(), new List()); - var actual = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actual); Assert.False(actual.HasImage); } [Fact] - public async void GetImage_Empty_NoLabeledAttachments() + public async void GetImage_InputWithUnlabeledAttachments_ReturnsNoImage() { var embeddedImageProvider = GetEmbeddedImageProvider(null); - var input = new Mock(); // add an attachment without a filename - has a list to look through but finds nothing - input.Setup(movie => movie.GetMediaSources(It.IsAny())) - .Returns(new List { new () { MediaAttachments = new List { new () } } }); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List()); + var input = GetMovie( + new List { new () }, + new List()); - var actual = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actual); Assert.False(actual.HasImage); } [Fact] - public async void GetImage_Empty_NoEmbeddedLabeledBackdrop() - { - var embeddedImageProvider = GetEmbeddedImageProvider(null); - - var input = new Mock(); - input.Setup(movie => movie.GetMediaSources(It.IsAny())) - .Returns(new List()); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List { new () { Type = MediaStreamType.EmbeddedImage } }); - - var actual = await embeddedImageProvider.GetImage(input.Object, ImageType.Backdrop, CancellationToken.None); - Assert.NotNull(actual); - Assert.False(actual.HasImage); - } - - [Fact] - public async void GetImage_Attached() + public async void GetImage_InputWithLabeledAttachments_ReturnsCorrectSelection() { // first tests file extension detection, second uses mimetype, third defaults to jpg MediaAttachment sampleAttachment1 = new () { FileName = "clearlogo.png", Index = 1 }; @@ -124,25 +101,23 @@ namespace Jellyfin.Providers.Tests.MediaInfo .Returns(Task.FromResult(targetPath3)); var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object); - var input = new Mock(); - input.Setup(movie => movie.GetMediaSources(It.IsAny())) - .Returns(new List { new () { MediaAttachments = new List { sampleAttachment1, sampleAttachment2, sampleAttachment3 } } }); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List()); + var input = GetMovie( + new List { sampleAttachment1, sampleAttachment2, sampleAttachment3 }, + new List()); - var actualLogo = await embeddedImageProvider.GetImage(input.Object, ImageType.Logo, CancellationToken.None); + var actualLogo = await embeddedImageProvider.GetImage(input, ImageType.Logo, CancellationToken.None); Assert.NotNull(actualLogo); Assert.True(actualLogo.HasImage); Assert.Equal(targetPath1, actualLogo.Path); Assert.Equal(ImageFormat.Png, actualLogo.Format); - var actualBackdrop = await embeddedImageProvider.GetImage(input.Object, ImageType.Backdrop, CancellationToken.None); + var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None); Assert.NotNull(actualBackdrop); Assert.True(actualBackdrop.HasImage); Assert.Equal(targetPath2, actualBackdrop.Path); Assert.Equal(ImageFormat.Bmp, actualBackdrop.Format); - var actualPrimary = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actualPrimary); Assert.True(actualPrimary.HasImage); Assert.Equal(targetPath3, actualPrimary.Path); @@ -150,23 +125,35 @@ namespace Jellyfin.Providers.Tests.MediaInfo } [Fact] - public async void GetImage_EmbeddedDefault() + public async void GetImage_InputWithUnlabeledEmbeddedImages_BackdropReturnsNoImage() + { + var embeddedImageProvider = GetEmbeddedImageProvider(null); + + var input = GetMovie( + new List(), + new List { new () { Type = MediaStreamType.EmbeddedImage } }); + + var actual = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None); + Assert.NotNull(actual); + Assert.False(actual.HasImage); + } + + [Fact] + public async void GetImage_InputWithUnlabeledEmbeddedImages_PrimaryReturnsImage() { MediaStream sampleStream = new () { Type = MediaStreamType.EmbeddedImage, Index = 1 }; string targetPath = "path"; var mediaEncoder = new Mock(MockBehavior.Strict); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), sampleStream, 1, "jpg", CancellationToken.None)) + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), sampleStream, 1, ".jpg", CancellationToken.None)) .Returns(Task.FromResult(targetPath)); var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object); - var input = new Mock(); - input.Setup(movie => movie.GetMediaSources(It.IsAny())) - .Returns(new List()); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List() { sampleStream }); + var input = GetMovie( + new List(), + new List { sampleStream }); - var actual = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actual); Assert.True(actual.HasImage); Assert.Equal(targetPath, actual.Path); @@ -174,7 +161,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo } [Fact] - public async void GetImage_EmbeddedSelection() + public async void GetImage_InputWithLabeledEmbeddedImages_ReturnsCorrectSelection() { // primary is second stream to ensure it's not defaulting, backdrop is first MediaStream sampleStream1 = new () { Type = MediaStreamType.EmbeddedImage, Index = 1, Comment = "backdrop" }; @@ -183,29 +170,47 @@ namespace Jellyfin.Providers.Tests.MediaInfo string targetPath2 = "path2.jpg"; var mediaEncoder = new Mock(MockBehavior.Strict); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), sampleStream1, 1, "jpg", CancellationToken.None)) + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), sampleStream1, 1, ".jpg", CancellationToken.None)) .Returns(Task.FromResult(targetPath1)); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), sampleStream2, 2, "jpg", CancellationToken.None)) + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), sampleStream2, 2, ".jpg", CancellationToken.None)) .Returns(Task.FromResult(targetPath2)); var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object); - var input = new Mock(); - input.Setup(movie => movie.GetMediaSources(It.IsAny())) - .Returns(new List()); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List { sampleStream1, sampleStream2 }); + var input = GetMovie( + new List(), + new List { sampleStream1, sampleStream2 }); - var actualPrimary = await embeddedImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actualPrimary); Assert.True(actualPrimary.HasImage); Assert.Equal(targetPath2, actualPrimary.Path); Assert.Equal(ImageFormat.Jpg, actualPrimary.Format); - var actualBackdrop = await embeddedImageProvider.GetImage(input.Object, ImageType.Backdrop, CancellationToken.None); + var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None); Assert.NotNull(actualBackdrop); Assert.True(actualBackdrop.HasImage); Assert.Equal(targetPath1, actualBackdrop.Path); Assert.Equal(ImageFormat.Jpg, actualBackdrop.Format); } + + private static EmbeddedImageProvider GetEmbeddedImageProvider(IMediaEncoder? mediaEncoder) + { + return new EmbeddedImageProvider(mediaEncoder); + } + + private static Movie GetMovie(List mediaAttachments, List mediaStreams) + { + // Mocking IMediaSourceManager GetMediaAttachments and GetMediaStreams instead of mocking Movie works, but + // has concurrency problems between this and VideoImageProviderTests due to BaseItem.MediaSourceManager + // being static + var movie = new Mock(); + + movie.Setup(item => item.GetMediaSources(It.IsAny())) + .Returns(new List { new () { MediaAttachments = mediaAttachments } } ); + movie.Setup(item => item.GetMediaStreams()) + .Returns(mediaStreams); + + return movie.Object; + } } } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs index 9a5cd79bb..0f51a2b8f 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs @@ -16,56 +16,51 @@ namespace Jellyfin.Providers.Tests.MediaInfo { public class VideoImageProviderTests { - private VideoImageProvider GetVideoImageProvider(IMediaEncoder? mediaEncoder) - { - // strict to ensure this isn't accidentally used where a prepared mock is intended - mediaEncoder ??= new Mock(MockBehavior.Strict).Object; - return new VideoImageProvider(mediaEncoder, new NullLogger()); - } - [Fact] - public async void GetImage_Empty_IsPlaceholder() + public async void GetImage_InputIsPlaceholder_ReturnsNoImage() { var videoImageProvider = GetVideoImageProvider(null); - var input = new Mock(); - input.Object.IsPlaceHolder = true; + var input = new Movie + { + IsPlaceHolder = true + }; - var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actual); Assert.False(actual.HasImage); } [Fact] - public async void GetImage_Empty_NoDefaultVideoStream() + public async void GetImage_NoDefaultVideoStream_ReturnsNoImage() { var videoImageProvider = GetVideoImageProvider(null); - var input = new Mock(); + var input = new Movie + { + DefaultVideoStreamIndex = null + }; - var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actual); Assert.False(actual.HasImage); } [Fact] - public async void GetImage_Empty_DefaultSet_NoVideoStream() + public async void GetImage_DefaultSetButNoVideoStream_ReturnsNoImage() { var videoImageProvider = GetVideoImageProvider(null); - var input = new Mock(); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List()); // set a default index but don't put anything there (invalid input, but provider shouldn't break) - input.Object.DefaultVideoStreamIndex = 1; + var input = GetMovie(0, null, new List()); - var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actual); Assert.False(actual.HasImage); } [Fact] - public async void GetImage_Extract_DefaultStream() + public async void GetImage_DefaultSetMultipleVideoStreams_ReturnsDefaultStreamImage() { MediaStream firstStream = new () { Type = MediaStreamType.Video, Index = 0 }; MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 1 }; @@ -78,14 +73,9 @@ namespace Jellyfin.Providers.Tests.MediaInfo .Returns(Task.FromResult(targetPath)); var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object); - var input = new Mock(); - input.Setup(movie => movie.GetDefaultVideoStream()) - .Returns(targetStream); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List() { firstStream, targetStream }); - input.Object.DefaultVideoStreamIndex = 1; + var input = GetMovie(1, targetStream, new List { firstStream, targetStream } ); - var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actual); Assert.True(actual.HasImage); Assert.Equal(targetPath, actual.Path); @@ -93,7 +83,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo } [Fact] - public async void GetImage_Extract_FallbackToFirstVideoStream() + public async void GetImage_InvalidDefaultSingleVideoStream_ReturnsFirstVideoStreamImage() { MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 }; string targetPath = "path.jpg"; @@ -103,13 +93,10 @@ namespace Jellyfin.Providers.Tests.MediaInfo .Returns(Task.FromResult(targetPath)); var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object); - var input = new Mock(); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List() { targetStream }); - // default must be set, ensure a stream is still found if not pointed at a video - input.Object.DefaultVideoStreamIndex = 5; + // provide query results for default (empty) and all streams (populated) + var input = GetMovie(5, null, new List { targetStream }); - var actual = await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.NotNull(actual); Assert.True(actual.HasImage); Assert.Equal(targetPath, actual.Path); @@ -117,10 +104,12 @@ namespace Jellyfin.Providers.Tests.MediaInfo } [Fact] - public async void GetImage_Time_Default() + public async void GetImage_NoTimeSpanSet_CallsEncoderWithDefaultTime() { MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 }; + // use a callback to catch the actual value + // provides more information on failure than verifying a specific input was called on the mock TimeSpan? actualTimeSpan = null; var mediaEncoder = new Mock(MockBehavior.Strict); mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), CancellationToken.None)) @@ -128,20 +117,16 @@ namespace Jellyfin.Providers.Tests.MediaInfo .Returns(Task.FromResult("path")); var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object); - var input = new Mock(); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List() { targetStream }); - // default must be set - input.Object.DefaultVideoStreamIndex = 0; + var input = GetMovie(0, targetStream, new List { targetStream }); // not testing return, just verifying what gets requested for time span - await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.Equal(TimeSpan.FromSeconds(10), actualTimeSpan); } [Fact] - public async void GetImage_Time_Calculated() + public async void GetImage_TimeSpanSet_CallsEncoderWithCalculatedTime() { MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 }; @@ -152,17 +137,40 @@ namespace Jellyfin.Providers.Tests.MediaInfo .Returns(Task.FromResult("path")); var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object); - var input = new Mock(); - input.Setup(movie => movie.GetMediaStreams()) - .Returns(new List() { targetStream }); - // default must be set - input.Object.DefaultVideoStreamIndex = 0; - input.Object.RunTimeTicks = 5000; + var input = GetMovie(0, targetStream, new List { targetStream }); + input.RunTimeTicks = 5000; // not testing return, just verifying what gets requested for time span - await videoImageProvider.GetImage(input.Object, ImageType.Primary, CancellationToken.None); + await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None); Assert.Equal(TimeSpan.FromTicks(500), actualTimeSpan); } + + private static VideoImageProvider GetVideoImageProvider(IMediaEncoder? mediaEncoder) + { + // strict to ensure this isn't accidentally used where a prepared mock is intended + mediaEncoder ??= new Mock(MockBehavior.Strict).Object; + return new VideoImageProvider(mediaEncoder, new NullLogger()); + } + + private static Movie GetMovie(int defaultVideoStreamIndex, MediaStream? defaultStream, List mediaStreams) + { + // Mocking IMediaSourceManager GetMediaStreams instead of mocking Movie works, but has concurrency problems + // between this and EmbeddedImageProviderTests due to BaseItem.MediaSourceManager being static + var movie = new Mock + { + Object = + { + DefaultVideoStreamIndex = defaultVideoStreamIndex + } + }; + + movie.Setup(item => item.GetDefaultVideoStream()) + .Returns(defaultStream!); + movie.Setup(item => item.GetMediaStreams()) + .Returns(mediaStreams); + + return movie.Object; + } } } -- cgit v1.2.3 From 1d19a5be617c191a731b76e556fae1e395eb3788 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 9 Nov 2021 22:29:33 +0100 Subject: Fix some warnings down to 580 --- Emby.Dlna/DlnaManager.cs | 19 +--- Emby.Dlna/Main/DlnaEntryPoint.cs | 7 +- Emby.Server.Implementations/Dto/DtoService.cs | 7 +- Emby.Server.Implementations/IO/LibraryMonitor.cs | 2 +- .../Library/LibraryManager.cs | 3 +- .../Library/Resolvers/Audio/MusicAlbumResolver.cs | 2 +- .../LiveTv/EmbyTV/EmbyTV.cs | 2 +- .../LiveTv/EmbyTV/EncodedRecorder.cs | 5 +- .../ScheduledTasks/ScheduledTaskWorker.cs | 10 +- .../Updates/InstallationManager.cs | 2 +- Jellyfin.Api/Controllers/DynamicHlsController.cs | 2 +- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 6 +- .../Models/PlaybackDtos/TranscodingThrottler.cs | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 4 +- .../MediaEncoding/IMediaEncoder.cs | 26 ----- .../Parsers/BaseItemXmlParser.cs | 2 +- .../Attachments/AttachmentExtractor.cs | 7 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 117 +-------------------- .../Subtitles/SubtitleEncoder.cs | 11 +- .../Parsers/SeriesNfoParser.cs | 2 +- jellyfin.ruleset | 4 + 21 files changed, 38 insertions(+), 204 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index f37d2d7d7..277a0e678 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -112,7 +112,7 @@ namespace Emby.Dlna if (profile == null) { - LogUnmatchedProfile(deviceInfo); + _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo); } else { @@ -122,23 +122,6 @@ namespace Emby.Dlna return profile; } - private void LogUnmatchedProfile(DeviceIdentification profile) - { - var builder = new StringBuilder(); - - builder.AppendLine("No matching device profile found. The default will need to be used."); - builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName); - builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer); - builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl); - builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription); - builder.Append("ModelName: ").AppendLine(profile.ModelName); - builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber); - builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl); - builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber); - - _logger.LogInformation(builder.ToString()); - } - /// /// Attempts to match a device with a profile. /// Rules: diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 8e89d9ae6..722428c73 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -218,11 +218,6 @@ namespace Emby.Dlna.Main } } - private void LogMessage(string msg) - { - _logger.LogDebug(msg); - } - private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer) { try @@ -272,7 +267,7 @@ namespace Emby.Dlna.Main Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost) { - LogFunction = LogMessage, + LogFunction = (msg) => _logger.LogDebug("{Msg}", msg), SupportPnpRootDevice = false }; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index c6b32a52c..67ecd04e0 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -134,14 +134,11 @@ namespace Emby.Server.Implementations.Dto var dto = GetBaseItemDtoInternal(item, options, user, owner); if (item is LiveTvChannel tvChannel) { - var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) }; - LivetvManager.AddChannelInfo(list, options, user); + LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user); } else if (item is LiveTvProgram) { - var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) }; - var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user); - Task.WaitAll(task); + LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult(); } if (item is IItemByName itemByName diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 7ebc800b9..b525f5a2f 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.IO if (_fileSystemWatchers.TryAdd(path, newWatcher)) { newWatcher.EnableRaisingEvents = true; - _logger.LogInformation("Watching directory " + path); + _logger.LogInformation("Watching directory {Path}", path); } else { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 2dbb569c6..559da7f5c 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -333,8 +333,7 @@ namespace Emby.Server.Implementations.Library { try { - var task = BaseItem.ChannelManager.DeleteItem(item); - Task.WaitAll(task); + BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult(); } catch (ArgumentException) { diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 60720dd2f..9e3f62276 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -151,7 +151,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { if (parser.IsMultiPart(path)) { - logger.LogDebug("Found multi-disc folder: " + path); + logger.LogDebug("Found multi-disc folder: {Path}", path); Interlocked.Increment(ref discSubfolderCount); } else diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 367f3cb9e..644f9050d 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -957,7 +957,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public async Task GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List currentLiveStreams, CancellationToken cancellationToken) { - _logger.LogInformation("Streaming Channel " + channelId); + _logger.LogInformation("Streaming Channel {Id}", channelId); var result = string.IsNullOrEmpty(streamId) ? null : diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 8688688e9..5726d7158 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -87,8 +87,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV ErrorDialog = false }; - var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments; - _logger.LogInformation(commandLineLogMessage); + _logger.LogInformation("{Filename} {Arguments}", processStartInfo.FileName, processStartInfo.Arguments); var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt"); Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); @@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false); - await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false); + await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + processStartInfo.FileName + " " + processStartInfo.Arguments + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false); _process = new Process { diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index f2cdfeb16..21a7f4f5f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -638,7 +638,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - _logger.LogInformation(Name + ": Cancelling"); + _logger.LogInformation("{Name}: Cancelling", Name); token.Cancel(); } catch (Exception ex) @@ -652,16 +652,16 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - _logger.LogInformation(Name + ": Waiting on Task"); + _logger.LogInformation("{Name}: Waiting on Task", Name); var exited = task.Wait(2000); if (exited) { - _logger.LogInformation(Name + ": Task exited"); + _logger.LogInformation("{Name}: Task exited", Name); } else { - _logger.LogInformation(Name + ": Timed out waiting for task to stop"); + _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name); } } catch (Exception ex) @@ -674,7 +674,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - _logger.LogDebug(Name + ": Disposing CancellationToken"); + _logger.LogDebug("{Name}: Disposing CancellationToken", Name); token.Dispose(); } catch (Exception ex) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 4a022c5db..ef95ebf94 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -571,7 +571,7 @@ namespace Emby.Server.Implementations.Updates ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version)); await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false); - _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version); + _logger.LogInformation("Plugin {Action}: {PluginName} {PluginVersion}", plugin == null ? "installed" : "updated", package.Name, package.Version); return plugin != null; } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 049fd503b..caa3d2368 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1391,7 +1391,7 @@ namespace Jellyfin.Api.Controllers } else { - _logger.LogError("Invalid HLS segment container: " + segmentFormat); + _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat); } var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index f435bbf00..9d80070eb 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -543,8 +543,7 @@ namespace Jellyfin.Api.Helpers state, cancellationTokenSource); - var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; - _logger.LogInformation(commandLineLogMessage); + _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); var logFilePrefix = "FFmpeg.Transcode-"; if (state.VideoRequest != null @@ -562,8 +561,9 @@ namespace Jellyfin.Api.Helpers // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); - await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); + await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false); process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state); diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs index 7b32d76ba..0136d9f86 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs @@ -197,7 +197,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos } } - _logger.LogDebug("No throttle data for " + path); + _logger.LogDebug("No throttle data for {Path}", path); return false; } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index ffd1c7f0a..ec1ebaabe 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -425,7 +425,7 @@ namespace MediaBrowser.Controller.Entities { if (item.IsFileProtocol) { - Logger.LogDebug("Removed item: " + item.Path); + Logger.LogDebug("Removed item: {Path}", item.Path); item.SetParent(null); LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false); @@ -807,7 +807,7 @@ namespace MediaBrowser.Controller.Entities { if (this is not ICollectionFolder) { - Logger.LogDebug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name); + Logger.LogDebug("{Type}: Query requires post-filtering due to LinkedChildren.", GetType().Name); return true; } } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index e6511ca8d..7d62fb6e1 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -100,32 +100,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// Location of video image. Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken); - /// - /// Extracts the video images on interval. - /// - /// Input file. - /// Video container type. - /// Media stream information. - /// Media source information. - /// Video 3D format. - /// Time interval. - /// Directory to write images. - /// Filename prefix to use. - /// Maximum width of image. - /// CancellationToken to use for operation. - /// A task. - Task ExtractVideoImagesOnInterval( - string inputFile, - string container, - MediaStream videoStream, - MediaSourceInfo mediaSource, - Video3DFormat? threedFormat, - TimeSpan interval, - string targetDirectory, - string filenamePrefix, - int? maxWidth, - CancellationToken cancellationToken); - /// /// Gets the media info. /// diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 80eb45423..777fe6774 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -149,7 +149,7 @@ namespace MediaBrowser.LocalMetadata.Parsers } else { - Logger.LogWarning("Invalid Added value found: " + val); + Logger.LogWarning("Invalid Added value found: {Value}", val); } } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index a524aeaa9..9ebc0d0cf 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -223,11 +223,10 @@ namespace MediaBrowser.MediaEncoding.Attachments if (failed) { - var msg = $"ffmpeg attachment extraction failed for {inputPath} to {outputPath}"; + _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); - _logger.LogError(msg); - - throw new InvalidOperationException(msg); + throw new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath)); } else { diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index fbc7ba72f..a2bac7b49 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -682,11 +682,9 @@ namespace MediaBrowser.MediaEncoding.Encoder if (exitCode == -1 || !file.Exists || file.Length == 0) { - var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath); + _logger.LogError("ffmpeg image extraction failed for {Path}", inputPath); - _logger.LogError(msg); - - throw new FfmpegException(msg); + throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath)); } return tempExtractPath; @@ -705,117 +703,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture); } - public async Task ExtractVideoImagesOnInterval( - string inputFile, - string container, - MediaStream videoStream, - MediaSourceInfo mediaSource, - Video3DFormat? threedFormat, - TimeSpan interval, - string targetDirectory, - string filenamePrefix, - int? maxWidth, - CancellationToken cancellationToken) - { - var inputArgument = GetInputArgument(inputFile, mediaSource); - - var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture); - - if (maxWidth.HasValue) - { - var maxWidthParam = maxWidth.Value.ToString(CultureInfo.InvariantCulture); - - vf += string.Format(CultureInfo.InvariantCulture, ",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam); - } - - Directory.CreateDirectory(targetDirectory); - var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg"); - - var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, _threads); - - if (!string.IsNullOrWhiteSpace(container)) - { - var inputFormat = EncodingHelper.GetInputFormat(container); - if (!string.IsNullOrWhiteSpace(inputFormat)) - { - args = "-f " + inputFormat + " " + args; - } - } - - var processStartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - FileName = _ffmpegPath, - Arguments = args, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }; - - _logger.LogInformation(processStartInfo.FileName + " " + processStartInfo.Arguments); - - await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - bool ranToCompletion = false; - - var process = new Process - { - StartInfo = processStartInfo, - EnableRaisingEvents = true - }; - using (var processWrapper = new ProcessWrapper(process, this)) - { - 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) - { - StopProcess(processWrapper, 1000); - } - } - finally - { - _thumbnailResourcePool.Release(); - } - - var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; - - if (exitCode == -1) - { - var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputArgument); - - _logger.LogError(msg); - - throw new FfmpegException(msg); - } - } - } - private void StartProcess(ProcessWrapper process) { process.Process.Start(); diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 2b2de2ff6..89365a516 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -636,17 +636,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (failed) { - var msg = $"ffmpeg subtitle extraction failed for {inputPath} to {outputPath}"; + _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); - _logger.LogError(msg); - - throw new FfmpegException(msg); + throw new FfmpegException( + string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath)); } else { - var msg = $"ffmpeg subtitle extraction completed for {inputPath} to {outputPath}"; - - _logger.LogInformation(msg); + _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); } if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index 2c893ac9f..3011d65a6 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -103,7 +103,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers } else { - Logger.LogInformation("Unrecognized series status: " + status); + Logger.LogInformation("Unrecognized series status: {Status}", status); } } diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 3bced438c..e14c1c427 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -44,9 +44,13 @@ + + + + -- cgit v1.2.3 From f73a7a6ed8554a188809c955ddccb48445f4dd71 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@gmail.com> Date: Fri, 12 Nov 2021 16:11:15 +0100 Subject: Use ImageFormat instead of string for extension --- .../MediaEncoding/IMediaEncoder.cs | 5 +-- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 36 ++++++++++++---------- .../MediaInfo/EmbeddedImageProvider.cs | 13 ++++---- .../MediaInfo/EmbeddedImageProviderTests.cs | 18 +++++------ 4 files changed, 38 insertions(+), 34 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs') diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 7d62fb6e1..1418e583e 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; @@ -95,10 +96,10 @@ namespace MediaBrowser.Controller.MediaEncoding /// Media source information. /// Media stream information. /// Index of the stream to extract from. - /// The extension of the file to write, including the '.'. + /// The format of the file to write. /// CancellationToken to use for operation. /// Location of video image. - Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken); + Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken); /// /// Gets the media info. diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index a2bac7b49..1c97a1982 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -19,6 +19,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -478,17 +479,17 @@ namespace MediaBrowser.MediaEncoding.Encoder Protocol = MediaProtocol.File }; - return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ".jpg", cancellationToken); + return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken); } public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { - return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ".jpg", cancellationToken); + return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ImageFormat.Jpg, cancellationToken); } - public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken) + public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken) { - return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, outputExtension, cancellationToken); + return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken); } private async Task ExtractImage( @@ -500,7 +501,7 @@ namespace MediaBrowser.MediaEncoding.Encoder bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, - string outputExtension, + ImageFormat? targetFormat, CancellationToken cancellationToken) { var inputArgument = GetInputArgument(inputFile, mediaSource); @@ -510,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter. try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, outputExtension, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, targetFormat, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -523,7 +524,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, outputExtension, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, targetFormat, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -536,7 +537,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, outputExtension, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, targetFormat, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -548,24 +549,25 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, outputExtension, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, targetFormat, cancellationToken).ConfigureAwait(false); } - private async Task ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, string outputExtension, CancellationToken cancellationToken) + private async Task ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, ImageFormat? targetFormat, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) { throw new ArgumentNullException(nameof(inputPath)); } - if (string.IsNullOrEmpty(outputExtension)) + var outputExtension = targetFormat switch { - outputExtension = ".jpg"; - } - else if (outputExtension[0] != '.') - { - outputExtension = "." + outputExtension; - } + ImageFormat.Bmp => ".bmp", + ImageFormat.Gif => ".gif", + ImageFormat.Jpg => ".jpg", + ImageFormat.Png => ".png", + ImageFormat.Webp => ".webp", + _ => ".jpg" + }; var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index ca0e72e49..79189416e 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -156,13 +156,14 @@ namespace MediaBrowser.Providers.MediaInfo } } + var format = ImageFormat.Jpg; string extractedImagePath = - await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, ".jpg", cancellationToken) + await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, format, cancellationToken) .ConfigureAwait(false); return new DynamicImageResponse { - Format = ImageFormat.Jpg, + Format = format, HasImage = true, Path = extractedImagePath, Protocol = MediaProtocol.File @@ -180,10 +181,6 @@ namespace MediaBrowser.Providers.MediaInfo extension = ".jpg"; } - string extractedAttachmentPath = - await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, extension, cancellationToken) - .ConfigureAwait(false); - ImageFormat format = extension switch { ".bmp" => ImageFormat.Bmp, @@ -194,6 +191,10 @@ namespace MediaBrowser.Providers.MediaInfo _ => ImageFormat.Jpg }; + string extractedAttachmentPath = + await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, format, cancellationToken) + .ConfigureAwait(false); + return new DynamicImageResponse { Format = format, diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs index 19391ba68..b6d6c3b25 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs @@ -57,7 +57,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo for (int i = 1; i <= targetIndex; i++) { var name = i == targetIndex ? filename : "unmatched"; - attachments.Add(new() + attachments.Add(new () { FileName = name, MimeType = mimetype, @@ -66,8 +66,8 @@ namespace Jellyfin.Providers.Tests.MediaInfo } var mediaEncoder = new Mock(MockBehavior.Strict); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((_, _, _, _, index, ext, _) => Task.FromResult(pathPrefix + index + ext)); + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((_, _, _, _, index, ext, _) => Task.FromResult(pathPrefix + index + "." + ext)); var embeddedImageProvider = new EmbeddedImageProvider(mediaEncoder.Object); var input = GetMovie(attachments, new List()); @@ -81,7 +81,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo else { Assert.True(actual.HasImage); - Assert.Equal(pathPrefix + targetIndex + "." + format, actual.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(pathPrefix + targetIndex + "." + format, actual.Path, StringComparer.OrdinalIgnoreCase); Assert.Equal(format, actual.Format); } } @@ -97,7 +97,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo for (int i = 1; i <= targetIndex; i++) { var comment = i == targetIndex ? label : "unmatched"; - streams.Add(new() + streams.Add(new () { Type = MediaStreamType.EmbeddedImage, Index = i, @@ -107,11 +107,11 @@ namespace Jellyfin.Providers.Tests.MediaInfo var pathPrefix = "path"; var mediaEncoder = new Mock(MockBehavior.Strict); - mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((_, _, _, stream, index, ext, _) => + mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((_, _, _, stream, index, ext, _) => { Assert.Equal(streams[index - 1], stream); - return Task.FromResult(pathPrefix + index + ext); + return Task.FromResult(pathPrefix + index + "." + ext); }); var embeddedImageProvider = new EmbeddedImageProvider(mediaEncoder.Object); @@ -122,7 +122,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo Assert.Equal(hasImage, actual.HasImage); if (hasImage) { - Assert.Equal(pathPrefix + targetIndex + ".jpg", actual.Path); + Assert.Equal(pathPrefix + targetIndex + ".jpg", actual.Path, StringComparer.OrdinalIgnoreCase); Assert.Equal(ImageFormat.Jpg, actual.Format); } } -- cgit v1.2.3