aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/codeql-analysis.yml8
-rw-r--r--.github/workflows/commands.yml4
-rw-r--r--.github/workflows/openapi.yml24
-rw-r--r--.github/workflows/repo-stale.yaml9
-rw-r--r--CONTRIBUTORS.md2
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs4
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj2
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs4
-rw-r--r--Emby.Dlna/Profiles/DefaultProfile.cs12
-rw-r--r--Emby.Drawing/ImageProcessor.cs569
-rw-r--r--Emby.Drawing/NullImageEncoder.cs58
-rw-r--r--Emby.Naming/Common/NamingOptions.cs3
-rw-r--r--Emby.Naming/Emby.Naming.csproj2
-rw-r--r--Emby.Notifications/Emby.Notifications.csproj2
-rw-r--r--Emby.Photos/Emby.Photos.csproj2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs16
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs25
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs53
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj6
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs10
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs54
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs30
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json37
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json42
-rw-r--r--Emby.Server.Implementations/Localization/Core/eu.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ka.json17
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json12
-rw-r--r--Emby.Server.Implementations/Localization/iso6392.txt1
-rw-r--r--Emby.Server.Implementations/Plugins/PluginLoadContext.cs33
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs18
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs4
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs11
-rw-r--r--Jellyfin.Api/Attributes/AcceptsFileAttribute.cs2
-rw-r--r--Jellyfin.Api/Attributes/ProducesFileAttribute.cs2
-rw-r--r--Jellyfin.Api/BaseJellyfinApiController.cs18
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs13
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs148
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs3
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs87
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs4
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs40
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs3
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs2
-rw-r--r--Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs (renamed from Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs)2
-rw-r--r--Jellyfin.Api/Formatters/CssOutputFormatter.cs (renamed from Jellyfin.Server/Formatters/CssOutputFormatter.cs)2
-rw-r--r--Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs (renamed from Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs)2
-rw-r--r--Jellyfin.Api/Formatters/XmlOutputFormatter.cs (renamed from Jellyfin.Server/Formatters/XmlOutputFormatter.cs)2
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs6
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj8
-rw-r--r--Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs (renamed from Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Middleware/ExceptionMiddleware.cs (renamed from Jellyfin.Server/Middleware/ExceptionMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs (renamed from Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Middleware/LanFilteringMiddleware.cs (renamed from Jellyfin.Server/Middleware/LanFilteringMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs (renamed from Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs (renamed from Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs (renamed from Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs (renamed from Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs (renamed from Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs (renamed from Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs)2
-rw-r--r--Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs (renamed from Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs)2
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs6
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs30
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs51
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs21
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs20
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj2
-rw-r--r--Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs36
-rw-r--r--Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs48
-rw-r--r--Jellyfin.Drawing.Skia/SkiaCodecException.cs45
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs545
-rw-r--r--Jellyfin.Drawing.Skia/SkiaException.cs39
-rw-r--r--Jellyfin.Drawing.Skia/SkiaHelper.cs47
-rw-r--r--Jellyfin.Drawing.Skia/SplashscreenBuilder.cs148
-rw-r--r--Jellyfin.Drawing.Skia/StripCollageBuilder.cs186
-rw-r--r--Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs64
-rw-r--r--Jellyfin.Networking/Jellyfin.Networking.csproj2
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs32
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs30
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs7
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj16
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs162
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbContext.cs188
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs2
-rw-r--r--Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs10
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs2
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthenticationManager.cs6
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs4
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs6
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs7
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs14
-rw-r--r--Jellyfin.Server/CoreAppHost.cs4
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs2
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs2
-rw-r--r--Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs90
-rw-r--r--Jellyfin.Server/Filters/FileRequestFilter.cs2
-rw-r--r--Jellyfin.Server/Filters/FileResponseFilter.cs2
-rw-r--r--Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs43
-rw-r--r--Jellyfin.Server/Helpers/StartupHelpers.cs326
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj14
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs1
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs49
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs4
-rw-r--r--Jellyfin.Server/Program.cs487
-rw-r--r--Jellyfin.Server/Startup.cs23
-rw-r--r--Jellyfin.Server/StartupOptions.cs8
-rw-r--r--Jellyfin.sln6
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs6
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj2
-rw-r--r--MediaBrowser.Common/Plugins/IPluginManager.cs5
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs1
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs54
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs2
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs32
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs10
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs5
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj8
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs44
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs363
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs8
-rw-r--r--MediaBrowser.Model/Dlna/MediaOptions.cs (renamed from MediaBrowser.Model/Dlna/AudioOptions.cs)54
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs386
-rw-r--r--MediaBrowser.Model/Dlna/VideoOptions.cs16
-rw-r--r--MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs23
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs11
-rw-r--r--MediaBrowser.Model/Extensions/EnumerableExtensions.cs17
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvOptions.cs4
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs5
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs3
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs40
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs19
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj8
-rw-r--r--MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs36
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs42
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs36
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs48
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs14
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj2
-rw-r--r--README.md8
-rw-r--r--debian/postinst10
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--fedora/jellyfin.spec3
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj4
-rw-r--r--fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj2
-rw-r--r--jellyfin.ruleset6
-rw-r--r--src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj (renamed from Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj)10
-rw-r--r--src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs35
-rw-r--r--src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs47
-rw-r--r--src/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs (renamed from Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs)0
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaCodecException.cs44
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs544
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaException.cs38
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaHelper.cs46
-rw-r--r--src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs147
-rw-r--r--src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs185
-rw-r--r--src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs63
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs595
-rw-r--r--src/Jellyfin.Drawing/Jellyfin.Drawing.csproj (renamed from Emby.Drawing/Emby.Drawing.csproj)10
-rw-r--r--src/Jellyfin.Drawing/NullImageEncoder.cs57
-rw-r--r--src/Jellyfin.Drawing/Properties/AssemblyInfo.cs (renamed from Emby.Drawing/Properties/AssemblyInfo.cs)2
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj7
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs24
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj2
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj2
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj10
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj6
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj8
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj8
-rw-r--r--tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs2
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj8
-rw-r--r--tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs6
-rw-r--r--tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj6
-rw-r--r--tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj6
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj8
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs34
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs101
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj8
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj8
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs1
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj8
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj8
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj8
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs64
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs2
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj10
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs22
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj10
-rw-r--r--tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj8
234 files changed, 4142 insertions, 3916 deletions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 5aebbae4d..6a04d1326 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
+ uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
+ uses: github/codeql-action/init@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
+ uses: github/codeql-action/autobuild@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
+ uses: github/codeql-action/analyze@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index f62ae853d..5d945c001 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
+ uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
+ uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index 889133aed..4577ff525 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,7 +14,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
+ uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -25,7 +25,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
+ uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
with:
name: openapi-head
retention-days: 14
@@ -39,9 +39,17 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
+ uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
- ref: ${{ github.base_ref }}
+ ref: ${{ github.event.pull_request.head.sha }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ fetch-depth: 0
+ - name: Checkout common ancestor
+ run: |
+ git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
+ git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
+ ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
+ git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
with:
@@ -49,7 +57,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
+ uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
with:
name: openapi-base
retention-days: 14
@@ -68,12 +76,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
+ uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
+ uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
with:
name: openapi-base
path: openapi-base
@@ -95,7 +103,7 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@f4499a714d59013c74a08789b48abe4b704364a0 # v2
+ uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index 1c6fe1492..897b7014a 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -5,13 +5,14 @@ on:
- cron: '30 1 * * *'
workflow_dispatch:
-permissions: {}
+permissions:
+ issues: write
jobs:
stale:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6
+ - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
@@ -22,7 +23,7 @@ jobs:
stale-issue-label: stale
stale-issue-message: |-
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
-
+
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
-
+
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 8daaae4d9..ec3c6fd2a 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -27,6 +27,7 @@
- [cvium](https://github.com/cvium)
- [dannymichel](https://github.com/dannymichel)
- [DaveChild](https://github.com/DaveChild)
+ - [DavidFair](https://github.com/DavidFair)
- [Delgan](https://github.com/Delgan)
- [dcrdev](https://github.com/dcrdev)
- [dhartung](https://github.com/dhartung)
@@ -37,6 +38,7 @@
- [DMouse10462](https://github.com/DMouse10462)
- [DrPandemic](https://github.com/DrPandemic)
- [eglia](https://github.com/eglia)
+ - [EgorBakanov](https://github.com/EgorBakanov)
- [EraYaN](https://github.com/EraYaN)
- [escabe](https://github.com/escabe)
- [excelite](https://github.com/excelite)
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index e9041186f..bea7a5a0d 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -195,7 +195,7 @@ namespace Emby.Dlna.Didl
{
var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user);
- streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
+ streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
{
ItemId = video.Id,
MediaSources = sources.ToArray(),
@@ -537,7 +537,7 @@ namespace Emby.Dlna.Didl
{
var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user);
- streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
+ streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
{
ItemId = audio.Id,
MediaSources = sources.ToArray(),
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 7ffb7118a..60e6dd644 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -28,7 +28,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 4cda1d8b7..7b1f942c5 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -585,7 +585,7 @@ namespace Emby.Dlna.PlayTo
{
return new PlaylistItem
{
- StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
+ StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
{
ItemId = item.Id,
MediaSources = mediaSources,
@@ -605,7 +605,7 @@ namespace Emby.Dlna.PlayTo
{
return new PlaylistItem
{
- StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
+ StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
{
ItemId = item.Id,
MediaSources = mediaSources,
diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs
index 23437f1bd..54a0a87a8 100644
--- a/Emby.Dlna/Profiles/DefaultProfile.cs
+++ b/Emby.Dlna/Profiles/DefaultProfile.cs
@@ -94,6 +94,12 @@ namespace Emby.Dlna.Profiles
new SubtitleProfile
{
+ Format = "sup",
+ Method = SubtitleDeliveryMethod.External
+ },
+
+ new SubtitleProfile
+ {
Format = "srt",
Method = SubtitleDeliveryMethod.Embed
},
@@ -142,6 +148,12 @@ namespace Emby.Dlna.Profiles
new SubtitleProfile
{
+ Format = "sup",
+ Method = SubtitleDeliveryMethod.Embed
+ },
+
+ new SubtitleProfile
+ {
Format = "subrip",
Method = SubtitleDeliveryMethod.Embed
},
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
deleted file mode 100644
index 5a49e876a..000000000
--- a/Emby.Drawing/ImageProcessor.cs
+++ /dev/null
@@ -1,569 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net.Mime;
-using System.Text;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Logging;
-using Photo = MediaBrowser.Controller.Entities.Photo;
-
-namespace Emby.Drawing
-{
- /// <summary>
- /// Class ImageProcessor.
- /// </summary>
- public sealed class ImageProcessor : IImageProcessor, IDisposable
- {
- // Increment this when there's a change requiring caches to be invalidated
- private const char Version = '3';
-
- private static readonly HashSet<string> _transparentImageTypes
- = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
-
- private readonly ILogger<ImageProcessor> _logger;
- private readonly IFileSystem _fileSystem;
- private readonly IServerApplicationPaths _appPaths;
- private readonly IImageEncoder _imageEncoder;
- private readonly IMediaEncoder _mediaEncoder;
-
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ImageProcessor"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="appPaths">The server application paths.</param>
- /// <param name="fileSystem">The filesystem.</param>
- /// <param name="imageEncoder">The image encoder.</param>
- /// <param name="mediaEncoder">The media encoder.</param>
- public ImageProcessor(
- ILogger<ImageProcessor> logger,
- IServerApplicationPaths appPaths,
- IFileSystem fileSystem,
- IImageEncoder imageEncoder,
- IMediaEncoder mediaEncoder)
- {
- _logger = logger;
- _fileSystem = fileSystem;
- _imageEncoder = imageEncoder;
- _mediaEncoder = mediaEncoder;
- _appPaths = appPaths;
- }
-
- private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
-
- /// <inheritdoc />
- public IReadOnlyCollection<string> SupportedInputFormats =>
- new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "tiff",
- "tif",
- "jpeg",
- "jpg",
- "png",
- "aiff",
- "cr2",
- "crw",
- "nef",
- "orf",
- "pef",
- "arw",
- "webp",
- "gif",
- "bmp",
- "erf",
- "raf",
- "rw2",
- "nrw",
- "dng",
- "ico",
- "astc",
- "ktx",
- "pkm",
- "wbmp"
- };
-
- /// <inheritdoc />
- public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
-
- /// <inheritdoc />
- public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
- {
- var file = await ProcessImage(options).ConfigureAwait(false);
- using (var fileStream = AsyncFile.OpenRead(file.Path))
- {
- await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
- }
- }
-
- /// <inheritdoc />
- public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
- => _imageEncoder.SupportedOutputFormats;
-
- /// <inheritdoc />
- public bool SupportsTransparency(string path)
- => _transparentImageTypes.Contains(Path.GetExtension(path));
-
- /// <inheritdoc />
- public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
- {
- ItemImageInfo originalImage = options.Image;
- BaseItem item = options.Item;
-
- string originalImagePath = originalImage.Path;
- DateTime dateModified = originalImage.DateModified;
- ImageDimensions? originalImageSize = null;
- if (originalImage.Width > 0 && originalImage.Height > 0)
- {
- originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
- }
-
- var mimeType = MimeTypes.GetMimeType(originalImagePath);
- if (!_imageEncoder.SupportsImageEncoding)
- {
- return (originalImagePath, mimeType, dateModified);
- }
-
- var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
- originalImagePath = supportedImageInfo.Path;
-
- // Original file doesn't exist, or original file is gif.
- if (!File.Exists(originalImagePath) || string.Equals(mimeType, MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase))
- {
- return (originalImagePath, mimeType, dateModified);
- }
-
- dateModified = supportedImageInfo.DateModified;
- bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
-
- bool autoOrient = false;
- ImageOrientation? orientation = null;
- if (item is Photo photo)
- {
- if (photo.Orientation.HasValue)
- {
- if (photo.Orientation.Value != ImageOrientation.TopLeft)
- {
- autoOrient = true;
- orientation = photo.Orientation;
- }
- }
- else
- {
- // Orientation unknown, so do it
- autoOrient = true;
- orientation = photo.Orientation;
- }
- }
-
- if (options.HasDefaultOptions(originalImagePath, originalImageSize) && (!autoOrient || !options.RequiresAutoOrientation))
- {
- // Just spit out the original file if all the options are default
- return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
- }
-
- int quality = options.Quality;
-
- ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
- string cacheFilePath = GetCacheFilePath(
- originalImagePath,
- options.Width,
- options.Height,
- options.MaxWidth,
- options.MaxHeight,
- options.FillWidth,
- options.FillHeight,
- quality,
- dateModified,
- outputFormat,
- options.AddPlayedIndicator,
- options.PercentPlayed,
- options.UnplayedCount,
- options.Blur,
- options.BackgroundColor,
- options.ForegroundLayer);
-
- try
- {
- if (!File.Exists(cacheFilePath))
- {
- string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
-
- if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
- {
- return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
- }
- }
-
- return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
- }
- catch (Exception ex)
- {
- // If it fails for whatever reason, return the original image
- _logger.LogError(ex, "Error encoding image");
- return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
- }
- }
-
- private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
- {
- var serverFormats = GetSupportedImageOutputFormats();
-
- // Client doesn't care about format, so start with webp if supported
- if (serverFormats.Contains(ImageFormat.Webp) && clientSupportedFormats.Contains(ImageFormat.Webp))
- {
- return ImageFormat.Webp;
- }
-
- // If transparency is needed and webp isn't supported, than png is the only option
- if (requiresTransparency && clientSupportedFormats.Contains(ImageFormat.Png))
- {
- return ImageFormat.Png;
- }
-
- foreach (var format in clientSupportedFormats)
- {
- if (serverFormats.Contains(format))
- {
- return format;
- }
- }
-
- // We should never actually get here
- return ImageFormat.Jpg;
- }
-
- private string GetMimeType(ImageFormat format, string path)
- => format switch
- {
- ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
- ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
- ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
- ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
- ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
- _ => MimeTypes.GetMimeType(path)
- };
-
- /// <summary>
- /// Gets the cache file path based on a set of parameters.
- /// </summary>
- private string GetCacheFilePath(
- string originalPath,
- int? width,
- int? height,
- int? maxWidth,
- int? maxHeight,
- int? fillWidth,
- int? fillHeight,
- int quality,
- DateTime dateModified,
- ImageFormat format,
- bool addPlayedIndicator,
- double percentPlayed,
- int? unwatchedCount,
- int? blur,
- string backgroundColor,
- string foregroundLayer)
- {
- var filename = new StringBuilder(256);
- filename.Append(originalPath);
-
- filename.Append(",quality=");
- filename.Append(quality);
-
- filename.Append(",datemodified=");
- filename.Append(dateModified.Ticks);
-
- filename.Append(",f=");
- filename.Append(format);
-
- if (width.HasValue)
- {
- filename.Append(",width=");
- filename.Append(width.Value);
- }
-
- if (height.HasValue)
- {
- filename.Append(",height=");
- filename.Append(height.Value);
- }
-
- if (maxWidth.HasValue)
- {
- filename.Append(",maxwidth=");
- filename.Append(maxWidth.Value);
- }
-
- if (maxHeight.HasValue)
- {
- filename.Append(",maxheight=");
- filename.Append(maxHeight.Value);
- }
-
- if (fillWidth.HasValue)
- {
- filename.Append(",fillwidth=");
- filename.Append(fillWidth.Value);
- }
-
- if (fillHeight.HasValue)
- {
- filename.Append(",fillheight=");
- filename.Append(fillHeight.Value);
- }
-
- if (addPlayedIndicator)
- {
- filename.Append(",pl=true");
- }
-
- if (percentPlayed > 0)
- {
- filename.Append(",p=");
- filename.Append(percentPlayed);
- }
-
- if (unwatchedCount.HasValue)
- {
- filename.Append(",p=");
- filename.Append(unwatchedCount.Value);
- }
-
- if (blur.HasValue)
- {
- filename.Append(",blur=");
- filename.Append(blur.Value);
- }
-
- if (!string.IsNullOrEmpty(backgroundColor))
- {
- filename.Append(",b=");
- filename.Append(backgroundColor);
- }
-
- if (!string.IsNullOrEmpty(foregroundLayer))
- {
- filename.Append(",fl=");
- filename.Append(foregroundLayer);
- }
-
- filename.Append(",v=");
- filename.Append(Version);
-
- return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
- }
-
- /// <inheritdoc />
- public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info)
- {
- int width = info.Width;
- int height = info.Height;
-
- if (height > 0 && width > 0)
- {
- return new ImageDimensions(width, height);
- }
-
- string path = info.Path;
- _logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
-
- ImageDimensions size = GetImageDimensions(path);
- info.Width = size.Width;
- info.Height = size.Height;
-
- return size;
- }
-
- /// <inheritdoc />
- public ImageDimensions GetImageDimensions(string path)
- => _imageEncoder.GetImageSize(path);
-
- /// <inheritdoc />
- public string GetImageBlurHash(string path)
- {
- var size = GetImageDimensions(path);
- return GetImageBlurHash(path, size);
- }
-
- /// <inheritdoc />
- public string GetImageBlurHash(string path, ImageDimensions imageDimensions)
- {
- if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0)
- {
- return string.Empty;
- }
-
- // We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
- // One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
- // See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
- float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
- float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
-
- int xComp = Math.Min((int)xCompF + 1, 9);
- int yComp = Math.Min((int)yCompF + 1, 9);
-
- return _imageEncoder.GetImageBlurHash(xComp, yComp, path);
- }
-
- /// <inheritdoc />
- public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
- => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
-
- /// <inheritdoc />
- public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
- {
- return GetImageCacheTag(item, new ItemImageInfo
- {
- Path = chapter.ImagePath,
- Type = ImageType.Chapter,
- DateModified = chapter.ImageDateModified
- });
- }
-
- /// <inheritdoc />
- public string? GetImageCacheTag(User user)
- {
- if (user.ProfileImage is null)
- {
- return null;
- }
-
- return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
- .ToString("N", CultureInfo.InvariantCulture);
- }
-
- private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
- {
- var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
-
- // These are just jpg files renamed as tbn
- if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
- {
- return Task.FromResult((originalImagePath, dateModified));
- }
-
- // TODO _mediaEncoder.ConvertImage is not implemented
- // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
- // {
- // try
- // {
- // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
- //
- // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
- // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
- //
- // var file = _fileSystem.GetFileInfo(outputPath);
- // if (!file.Exists)
- // {
- // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
- // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
- // }
- // else
- // {
- // dateModified = file.LastWriteTimeUtc;
- // }
- //
- // originalImagePath = outputPath;
- // }
- // catch (Exception ex)
- // {
- // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
- // }
- // }
-
- return Task.FromResult((originalImagePath, dateModified));
- }
-
- /// <summary>
- /// Gets the cache path.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="uniqueName">Name of the unique.</param>
- /// <param name="fileExtension">The file extension.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">
- /// path
- /// or
- /// uniqueName
- /// or
- /// fileExtension.
- /// </exception>
- public string GetCachePath(string path, string uniqueName, string fileExtension)
- {
- ArgumentException.ThrowIfNullOrEmpty(path);
- ArgumentException.ThrowIfNullOrEmpty(uniqueName);
- ArgumentException.ThrowIfNullOrEmpty(fileExtension);
-
- var filename = uniqueName.GetMD5() + fileExtension;
-
- return GetCachePath(path, filename);
- }
-
- /// <summary>
- /// Gets the cache path.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="filename">The filename.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">
- /// path
- /// or
- /// filename.
- /// </exception>
- public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename)
- {
- if (path.IsEmpty)
- {
- throw new ArgumentException("Path can't be empty.", nameof(path));
- }
-
- if (filename.IsEmpty)
- {
- throw new ArgumentException("Filename can't be empty.", nameof(filename));
- }
-
- var prefix = filename.Slice(0, 1);
-
- return Path.Join(path, prefix, filename);
- }
-
- /// <inheritdoc />
- public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
- {
- _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
-
- _imageEncoder.CreateImageCollage(options, libraryName);
-
- _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- if (_imageEncoder is IDisposable disposable)
- {
- disposable.Dispose();
- }
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs
deleted file mode 100644
index d0a26b713..000000000
--- a/Emby.Drawing/NullImageEncoder.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Model.Drawing;
-
-namespace Emby.Drawing
-{
- /// <summary>
- /// A fallback implementation of <see cref="IImageEncoder" />.
- /// </summary>
- public class NullImageEncoder : IImageEncoder
- {
- /// <inheritdoc />
- public IReadOnlyCollection<string> SupportedInputFormats
- => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" };
-
- /// <inheritdoc />
- public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
- => new HashSet<ImageFormat>() { ImageFormat.Jpg, ImageFormat.Png };
-
- /// <inheritdoc />
- public string Name => "Null Image Encoder";
-
- /// <inheritdoc />
- public bool SupportsImageCollageCreation => false;
-
- /// <inheritdoc />
- public bool SupportsImageEncoding => false;
-
- /// <inheritdoc />
- public ImageDimensions GetImageSize(string path)
- => throw new NotImplementedException();
-
- /// <inheritdoc />
- public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
- {
- throw new NotImplementedException();
- }
-
- /// <inheritdoc />
- public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
- {
- throw new NotImplementedException();
- }
-
- /// <inheritdoc />
- public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
- {
- throw new NotImplementedException();
- }
-
- /// <inheritdoc />
- public string GetImageBlurHash(int xComp, int yComp, string path)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 0119fa38c..54f62a157 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -153,7 +153,7 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
- @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+ @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"^(?<cleaned>.+?)(\[.*\])",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@@ -169,6 +169,7 @@ namespace Emby.Naming.Common
".srt",
".ssa",
".sub",
+ ".sup",
".vtt",
};
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 80bc57a5d..3106e2246 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -47,7 +47,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 138965c89..eb269183e 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -23,7 +23,7 @@
<!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 34bc8f32f..ae6bc2db1 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -26,7 +26,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index d0c744d2f..e1e83621b 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -18,7 +18,6 @@ using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
-using Emby.Drawing;
using Emby.Naming.Common;
using Emby.Notifications;
using Emby.Photos;
@@ -45,6 +44,7 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
+using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
@@ -193,11 +193,6 @@ namespace Emby.Server.Implementations
/// </summary>
private string PublishedServerUrl => _startupConfig[AddressOverrideKey];
- /// <summary>
- /// Gets a value indicating whether this instance can self restart.
- /// </summary>
- public bool CanSelfRestart => _startupOptions.RestartPath is not null;
-
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser
@@ -654,7 +649,7 @@ namespace Emby.Server.Implementations
/// <returns>A task representing the service initialization operation.</returns>
public async Task InitializeServices()
{
- var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false);
+ var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
await using (jellyfinDb.ConfigureAwait(false))
{
if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
@@ -935,17 +930,13 @@ namespace Emby.Server.Implementations
/// </summary>
public void Restart()
{
- if (!CanSelfRestart)
- {
- throw new PlatformNotSupportedException("The server is unable to self-restart. Please restart manually.");
- }
-
if (IsShuttingDown)
{
return;
}
IsShuttingDown = true;
+ _pluginManager.UnloadAssemblies();
Task.Run(async () =>
{
@@ -1047,7 +1038,6 @@ namespace Emby.Server.Implementations
CachePath = ApplicationPaths.CachePath,
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
- CanSelfRestart = CanSelfRestart,
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index ff9aa4c2a..1d61667f8 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -61,10 +61,21 @@ namespace Emby.Server.Implementations.Data
protected virtual int? CacheSize => null;
/// <summary>
+ /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
+ /// </summary>
+ protected virtual string LockingMode => "EXCLUSIVE";
+
+ /// <summary>
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
/// </summary>
/// <value>The journal mode.</value>
- protected virtual string JournalMode => "TRUNCATE";
+ protected virtual string JournalMode => "WAL";
+
+ /// <summary>
+ /// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
+ /// </summary>
+ /// <value>The journal size limit.</value>
+ protected virtual int? JournalSizeLimit => 0;
/// <summary>
/// Gets the page size.
@@ -84,7 +95,7 @@ namespace Emby.Server.Implementations.Data
/// </summary>
/// <value>The synchronous mode or null.</value>
/// <see cref="SynchronousMode"/>
- protected virtual SynchronousMode? Synchronous => null;
+ protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
/// <summary>
/// Gets or sets the write lock.
@@ -116,11 +127,21 @@ namespace Emby.Server.Implementations.Data
WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
+ if (!string.IsNullOrWhiteSpace(LockingMode))
+ {
+ WriteConnection.Execute("PRAGMA locking_mode=" + LockingMode);
+ }
+
if (!string.IsNullOrWhiteSpace(JournalMode))
{
WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode);
}
+ if (JournalSizeLimit.HasValue)
+ {
+ WriteConnection.Execute("PRAGMA journal_size_limit=" + (int)JournalSizeLimit.Value);
+ }
+
if (Synchronous.HasValue)
{
WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 9bdc4e5c8..bc703fe90 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -359,8 +359,6 @@ namespace Emby.Server.Implementations.Data
string[] queries =
{
- "PRAGMA locking_mode=EXCLUSIVE",
-
"create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)",
"create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))",
@@ -385,39 +383,6 @@ namespace Emby.Server.Implementations.Data
string[] postQueries =
{
- // obsolete
- "drop index if exists idx_TypedBaseItems",
- "drop index if exists idx_mediastreams",
- "drop index if exists idx_mediastreams1",
- "drop index if exists idx_" + ChaptersTableName,
- "drop index if exists idx_UserDataKeys1",
- "drop index if exists idx_UserDataKeys2",
- "drop index if exists idx_TypeTopParentId3",
- "drop index if exists idx_TypeTopParentId2",
- "drop index if exists idx_TypeTopParentId4",
- "drop index if exists idx_Type",
- "drop index if exists idx_TypeTopParentId",
- "drop index if exists idx_GuidType",
- "drop index if exists idx_TopParentId",
- "drop index if exists idx_TypeTopParentId6",
- "drop index if exists idx_ItemValues2",
- "drop index if exists Idx_ProviderIds",
- "drop index if exists idx_ItemValues3",
- "drop index if exists idx_ItemValues4",
- "drop index if exists idx_ItemValues5",
- "drop index if exists idx_UserDataKeys3",
- "drop table if exists UserDataKeys",
- "drop table if exists ProviderIds",
- "drop index if exists Idx_ProviderIds1",
- "drop table if exists Images",
- "drop index if exists idx_Images",
- "drop index if exists idx_TypeSeriesPresentationUniqueKey",
- "drop index if exists idx_SeriesPresentationUniqueKey",
- "drop index if exists idx_TypeSeriesPresentationUniqueKey2",
- "drop index if exists idx_AncestorIds3",
- "drop index if exists idx_AncestorIds4",
- "drop index if exists idx_AncestorIds2",
-
"create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)",
"create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)",
@@ -458,6 +423,9 @@ namespace Emby.Server.Implementations.Data
// Used to update inherited tags
"create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)",
+
+ "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)",
+ "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)"
};
using (var connection = GetConnection())
@@ -2401,13 +2369,17 @@ namespace Emby.Server.Implementations.Data
var builder = new StringBuilder();
builder.Append('(');
- if (string.IsNullOrEmpty(item.OfficialRating))
+ if (item.InheritedParentalRatingValue == 0)
{
- builder.Append("(OfficialRating is null * 10)");
+ builder.Append("((InheritedParentalRatingValue=0) * 10)");
}
else
{
- builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
+ builder.Append(
+ @"(SELECT CASE WHEN InheritedParentalRatingValue=0
+ THEN 0
+ ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue))
+ END)");
}
if (item.ProductionYear.HasValue)
@@ -2521,6 +2493,11 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@SimilarItemId", item.Id);
}
+
+ if (commandText.Contains("@InheritedParentalRatingValue", StringComparison.OrdinalIgnoreCase))
+ {
+ statement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue);
+ }
}
private string GetJoinUserDataText(InternalItemsQuery query)
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index f46affc73..7eaef094b 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -18,7 +18,7 @@
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
- <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
+ <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
</ItemGroup>
@@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.1" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" />
<PackageReference Include="Mono.Nat" Version="3.0.4" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
@@ -54,7 +54,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index 3769ae4dd..b7bcaace1 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -21,16 +21,6 @@ namespace Emby.Server.Implementations
string? PackageName { get; }
/// <summary>
- /// Gets the value of the --restartpath command line option.
- /// </summary>
- string? RestartPath { get; }
-
- /// <summary>
- /// Gets the value of the --restartargs command line option.
- /// </summary>
- string? RestartArgs { get; }
-
- /// <summary>
/// Gets the value of the --published-server-url command line option.
/// </summary>
string? PublishedServerUrl { get; }
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 74c53b2da..6aef87c52 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -89,17 +89,7 @@ namespace Emby.Server.Implementations.Library
// Give some preference to external text subs for better performance
return streams
.Where(i => i.Type == type)
- .OrderBy(i =>
- {
- var index = languagePreferences.FindIndex(x => string.Equals(x, i.Language, StringComparison.OrdinalIgnoreCase));
-
- return index == -1 ? 100 : index;
- })
- .ThenBy(i => GetBooleanOrderBy(i.IsDefault))
- .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream))
- .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream))
- .ThenBy(i => GetBooleanOrderBy(i.IsExternal))
- .ThenBy(i => i.Index);
+ .OrderByDescending(i => GetStreamScore(i, languagePreferences));
}
public static void SetSubtitleStreamScores(
@@ -113,9 +103,9 @@ namespace Emby.Server.Implementations.Library
return;
}
- var sortedStreams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages);
+ var sortedStreams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages).ToList();
- var filteredStreams = new List<MediaStream>();
+ List<MediaStream>? filteredStreams = null;
if (mode == SubtitlePlaybackMode.Default)
{
@@ -144,46 +134,26 @@ namespace Emby.Server.Implementations.Library
}
// load forced subs if we have found no suitable full subtitles
- var iterStreams = filteredStreams.Count == 0
+ var iterStreams = filteredStreams is null || filteredStreams.Count == 0
? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
: filteredStreams;
foreach (var stream in iterStreams)
{
- stream.Score = GetSubtitleScore(stream, preferredLanguages);
+ stream.Score = GetStreamScore(stream, preferredLanguages);
}
}
- private static int GetSubtitleScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
+ internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
{
- var values = new List<int>();
-
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
-
- values.Add(index == -1 ? 0 : 100 - index);
-
- values.Add(stream.IsForced ? 1 : 0);
- values.Add(stream.IsDefault ? 1 : 0);
- values.Add(stream.SupportsExternalStream ? 1 : 0);
- values.Add(stream.IsTextSubtitleStream ? 1 : 0);
- values.Add(stream.IsExternal ? 1 : 0);
-
- values.Reverse();
- var scale = 1;
- var score = 0;
-
- foreach (var value in values)
- {
- score += scale * (value + 1);
- scale *= 10;
- }
-
+ var score = index == -1 ? 1 : 101 - index;
+ score = (score * 10) + (stream.IsForced ? 2 : 1);
+ score = (score * 10) + (stream.IsDefault ? 2 : 1);
+ score = (score * 10) + (stream.SupportsExternalStream ? 2 : 1);
+ score = (score * 10) + (stream.IsTextSubtitleStream ? 2 : 1);
+ score = (score * 10) + (stream.IsExternal ? 2 : 1);
return score;
}
-
- private static int GetBooleanOrderBy(bool value)
- {
- return value ? 0 : 1;
- }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 8f5fa8694..8edd8f66a 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1814,21 +1814,29 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
program.AddGenre("News");
}
- if (timer.IsProgramSeries)
- {
- await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false);
- await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false);
- }
- else if (!timer.IsMovie || timer.IsSports || timer.IsNews)
+ var config = GetConfiguration();
+
+ if (config.SaveRecordingNFO)
{
- await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false);
+ if (timer.IsProgramSeries)
+ {
+ await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false);
+ await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false);
+ }
+ else if (!timer.IsMovie || timer.IsSports || timer.IsNews)
+ {
+ await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false);
+ }
+ else
+ {
+ await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false);
+ }
}
- else
+
+ if (config.SaveRecordingImages)
{
- await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false);
+ await SaveRecordingImages(recordingPath, program).ConfigureAwait(false);
}
-
- await SaveRecordingImages(recordingPath, program).ConfigureAwait(false);
}
catch (Exception ex)
{
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index f356c98a9..9fbf364ef 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Optimaliseer databasis",
"TaskKeyframeExtractorDescription": "Haal keyframes vanuit video lêers om meer presiese HLS afspeellyste te maak. Dit kan lank duur.",
"TaskKeyframeExtractor": "Keyframe Ekstraktor",
- "External": "Ekstern"
+ "External": "Ekstern",
+ "HearingImpaired": "gehoorgestremd"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index ada3c7730..93d50e6e3 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -3,20 +3,20 @@
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق",
"Artists": "الفنانين",
- "AuthenticationSucceededWithUserName": "تمت مصادقة {0} بنجاح",
+ "AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}",
"Books": "الكتب",
- "CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}",
+ "CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}",
"Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
"Collections": "التجميعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}",
- "Favorites": "مفضلات",
+ "Favorites": "المفضلة",
"Folders": "المجلدات",
"Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم",
- "HeaderContinueWatching": "استمر بالمشاهدة",
+ "HeaderContinueWatching": "استئناف المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
@@ -27,15 +27,15 @@
"HeaderRecordingGroups": "مجموعات التسجيل",
"HomeVideos": "الفيديوهات الشخصية",
"Inherit": "توريث",
- "ItemAddedWithName": "تم إضافة {0} للمكتبة",
- "ItemRemovedWithName": "تم إزالة {0} من المكتبة",
+ "ItemAddedWithName": "أُضيف {0} للمكتبة",
+ "ItemRemovedWithName": "أُزيل {0} من المكتبة",
"LabelIpAddressValue": "عنوان الآي بي: {0}",
"LabelRunningTimeValue": "مدة التشغيل: {0}",
"Latest": "أحدث",
- "MessageApplicationUpdated": "لقد تم تحديث خادم Jellyfin",
- "MessageApplicationUpdatedTo": "تم تحديث خادم Jellyfin الى {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث إعدادات الخادم في قسم {0}",
- "MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم",
+ "MessageApplicationUpdated": "حُدث خادم Jellyfin",
+ "MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
+ "MessageServerConfigurationUpdated": "حُدثت إعدادات الخادم",
"MixedContent": "محتوى مختلط",
"Movies": "الأفلام",
"Music": "الموسيقى",
@@ -45,14 +45,14 @@
"NameSeasonUnknown": "الموسم غير معروف",
"NewVersionIsAvailable": "نسخة جديدة من خادم Jellyfin متوفرة للتحميل.",
"NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق",
- "NotificationOptionApplicationUpdateInstalled": "تم تحديث التطبيق",
+ "NotificationOptionApplicationUpdateInstalled": "نُصب تحديث التطبيق",
"NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
- "NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي",
- "NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا",
+ "NotificationOptionAudioPlaybackStopped": "أُوقف تشغيل المقطع الصوتي",
+ "NotificationOptionCameraImageUploaded": "رُفعت صورة الكاميرا",
"NotificationOptionInstallationFailed": "فشل في التثبيت",
- "NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد",
+ "NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
"NotificationOptionPluginError": "فشل في الملحق",
- "NotificationOptionPluginInstalled": "تم تثبيت الملحق",
+ "NotificationOptionPluginInstalled": "ثُبتت المكونات الإضافية",
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
"NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
@@ -91,13 +91,13 @@
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
"ValueSpecialEpisodeName": "حلقه خاصه - {0}",
- "VersionNumber": "النسخة {0}",
+ "VersionNumber": "الإصدار {0}",
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
"TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة",
"TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة",
- "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
+ "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يُحدث البيانات الوصفية.",
"TaskRefreshLibrary": "افحص مكتبة الوسائط",
"TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
"TaskRefreshChapterImages": "استخراج صور الفصل",
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "تحسين قاعدة البيانات",
"TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
- "External": "خارجي"
+ "External": "خارجي",
+ "HearingImpaired": "ضعاف السمع"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index c0ed01fdf..1966f6968 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -40,7 +40,7 @@
"Movies": "Pel·lícules",
"Music": "Música",
"MusicVideos": "Vídeos Musicals",
- "NameInstallFailed": "Instal·lació de {0} fallida",
+ "NameInstallFailed": "{0} instal·lació fallida",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconeguda",
"NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.",
@@ -118,7 +118,7 @@
"TaskCleanActivityLog": "Buidar Registre d'Activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
- "Default": "Defecte",
+ "Default": "Per defecte",
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
"TaskOptimizeDatabase": "Optimitzar la base de dades",
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 8e9287af4..c6e2244ca 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -15,7 +15,7 @@
"Favorites": "Αγαπημένα",
"Folders": "Φάκελοι",
"Genres": "Είδη",
- "HeaderAlbumArtists": "Δισκογραφικοί καλλιτέχνες",
+ "HeaderAlbumArtists": "Καλλιτέχνες άλμπουμ",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
@@ -24,8 +24,8 @@
"HeaderFavoriteSongs": "Αγαπημένα Τραγούδια",
"HeaderLiveTV": "Ζωντανή Τηλεόραση",
"HeaderNextUp": "Επόμενο",
- "HeaderRecordingGroups": "Μουσικά Συγκροτήματα",
- "HomeVideos": "Προσωπικά βίντεο",
+ "HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
+ "HomeVideos": "Προσωπικά Βίντεο",
"Inherit": "Κληρονόμηση",
"ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη",
"ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη",
@@ -51,10 +51,10 @@
"NotificationOptionCameraImageUploaded": "Μεταφορτώθηκε φωτογραφία απο κάμερα",
"NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης",
"NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο",
- "NotificationOptionPluginError": "Αποτυχία του plugin",
- "NotificationOptionPluginInstalled": "Το plugin εγκαταστάθηκε",
- "NotificationOptionPluginUninstalled": "Το plugin απεγκαταστάθηκε",
- "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του plugin εγκαταστάθηκε",
+ "NotificationOptionPluginError": "Αποτυχία του πρόσθετου",
+ "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε",
+ "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε",
+ "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε",
"NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση",
"NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας",
"NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε",
@@ -66,7 +66,7 @@
"PluginInstalledWithName": "{0} εγκαταστήθηκε",
"PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
"PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
- "ProviderValue": "Provider: {0}",
+ "ProviderValue": "Πάροχος: {0}",
"ScheduledTaskFailedWithName": "{0} αποτυχία",
"ScheduledTaskStartedWithName": "{0} ξεκίνησε",
"ServerNameNeedsToBeRestarted": "{0} χρειάζεται επανεκκίνηση",
@@ -79,7 +79,7 @@
"System": "Σύστημα",
"TvShows": "Τηλεοπτικές Σειρές",
"User": "Χρήστης",
- "UserCreatedWithName": "Δημιουργήθηκε ο χρήστης {0}",
+ "UserCreatedWithName": "Ο χρήστης {0} δημιουργήθηκε",
"UserDeletedWithName": "Ο χρήστης {0} έχει διαγραφεί",
"UserDownloadingItemWithValues": "{0} κατεβάζει {1}",
"UserLockedOutWithName": "Ο χρήστης {0} αποκλείστηκε",
@@ -93,29 +93,29 @@
"ValueSpecialEpisodeName": "Σπέσιαλ - {0}",
"VersionNumber": "Έκδοση {0}",
"TaskRefreshPeople": "Ανανέωση Ατόμων",
- "TaskCleanLogsDescription": "Διαγράφει τα αρχεία καταγραφής που είναι άνω των {0} ημερών.",
- "TaskCleanLogs": "Καθαρισμός Καταλόγου Καταγραφής",
- "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και αναζωογονεί τα μεταδεδομένα.",
+ "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
+ "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής",
+ "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.",
"TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων",
- "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο με κεφάλαια.",
+ "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.",
"TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
- "TaskCleanCacheDescription": "Τα διαγραμμένα αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον από το σύστημα.",
+ "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.",
"TaskCleanCache": "Καθαρισμός Καταλόγου Προσωρινής Μνήμης",
"TasksChannelsCategory": "Κανάλια Διαδικτύου",
"TasksApplicationCategory": "Εφαρμογή",
"TasksLibraryCategory": "Βιβλιοθήκη",
"TasksMaintenanceCategory": "Συντήρηση",
- "TaskDownloadMissingSubtitlesDescription": "Αναζητήσεις στο διαδίκτυο όπου λείπουν υπότιτλους με βάση τη διαμόρφωση μεταδεδομένων.",
+ "TaskDownloadMissingSubtitlesDescription": "Ψάχνει στο διαδίκτυο για υπότιτλους που λείπουν με βάση τη διαμόρφωση μεταδεδομένων.",
"TaskDownloadMissingSubtitles": "Λήψη υπότιτλων που λείπουν",
"TaskRefreshChannelsDescription": "Ανανεώνει τις πληροφορίες καναλιού στο διαδικτύου.",
"TaskRefreshChannels": "Ανανέωση Καναλιών",
- "TaskCleanTranscodeDescription": "Διαγράφει αρχείου διακωδικοποιητή περισσότερο από μία ημέρα.",
- "TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή",
- "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
- "TaskUpdatePlugins": "Ενημέρωση Προσθηκών",
- "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.",
+ "TaskCleanTranscodeDescription": "Διαγράφει αρχεία διακωδικοποίησης άνω της μίας ημέρας.",
+ "TaskCleanTranscode": "Εκκαθάριση Kαταλόγου Διακωδικοποίησης",
+ "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τα πρόσθετα που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
+ "TaskUpdatePlugins": "Ενημέρωση Πρόσθετων",
+ "TaskRefreshPeopleDescription": "Ενημερώνει τα μεταδεδομένα για ηθοποιούς και σκηνοθέτες στη βιβλιοθήκη πολυμέσων σας.",
"TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής παλαιότερες από την επιλεγμένη ηλικία.",
- "TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
+ "TaskCleanActivityLog": "Εκκαθάριση Αρχείου Καταγραφής Δραστηριοτήτων",
"Undefined": "Απροσδιόριστο",
"Forced": "Εξαναγκασμένο",
"Default": "Προεπιλογή",
diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json
index d657ac7b6..e91084f92 100644
--- a/Emby.Server.Implementations/Localization/Core/eu.json
+++ b/Emby.Server.Implementations/Localization/Core/eu.json
@@ -100,7 +100,7 @@
"ItemRemovedWithName": "{0} liburutegitik ezabatu da",
"ItemAddedWithName": "{0} liburutegira gehitu da",
"HomeVideos": "Etxeko bideoak",
- "HeaderNextUp": "Hurrengoa",
+ "HeaderNextUp": "Nobedadeak",
"HeaderLiveTV": "Zuzeneko TB",
"HeaderFavoriteSongs": "Gogoko abestiak",
"HeaderFavoriteShows": "Gogoko showak",
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index d90d705b2..7f616c35a 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "データベースの最適化",
"TaskKeyframeExtractorDescription": "より正確なHLSプレイリストを作成するため、動画ファイルからキーフレームを抽出する。この処理には時間がかかる場合があります。",
"TaskKeyframeExtractor": "キーフレーム抽出",
- "External": "外部"
+ "External": "外部",
+ "HearingImpaired": "聴覚障害の方"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json
index 3a8b89f44..dbbc81eeb 100644
--- a/Emby.Server.Implementations/Localization/Core/ka.json
+++ b/Emby.Server.Implementations/Localization/Core/ka.json
@@ -108,5 +108,20 @@
"UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია",
"UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა",
"UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე",
- "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა."
+ "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.",
+ "TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.",
+ "NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.",
+ "CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან",
+ "StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.",
+ "SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა",
+ "ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას",
+ "TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველი ჟურნალის ჩანაწერების წაშლა.",
+ "TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.",
+ "TaskRefreshLibraryDescription": "თქვენი მედია ბიბლიოთეკაში ახალი ფაილების ძებნა და მეტამონაცემების განახლება.",
+ "TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.",
+ "TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.",
+ "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
+ "TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
+ "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.",
+ "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index a4b2e75b3..67dcf5b04 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "데이터베이스 최적화",
"TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.",
"TaskKeyframeExtractor": "키프레임 추출",
- "External": "외부"
+ "External": "외부",
+ "HearingImpaired": "청각 장애"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index d0b458a8f..d4c15ac87 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "Kompaktuje bazę danych i obcina wolne miejsce. Uruchomienie tego zadania po przeskanowaniu biblioteki lub dokonaniu innych zmian, które pociągają za sobą modyfikacje bazy danych, może poprawić wydajność.",
"External": "Zewnętrzny",
"TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.",
- "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych"
+ "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
+ "HearingImpaired": "Niedosłyszący"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index 506c14fdc..466c8a990 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -13,5 +13,11 @@
"DeviceOfflineWithName": "{0} abandoned ship",
"AppDeviceValues": "Captain: {0}, Ship: {1}",
"CameraImageUploadedFrom": "Yer looking glass has glimpsed another painting from {0}",
- "Collections": "Barrels"
+ "Collections": "Barrels",
+ "ItemAddedWithName": "{0} is now with yer treasure",
+ "Default": "Normal-like",
+ "FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}",
+ "Favorites": "Finest Loot",
+ "ItemRemovedWithName": "{0} was taken from yer treasure",
+ "LabelIpAddressValue": "Ship's coordinates: {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index dc45a8264..65cf29e80 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -1,6 +1,6 @@
{
"Albums": "Альбомы",
- "AppDeviceValues": "Приложение.: {0}, Устройство.: {1}",
+ "AppDeviceValues": "Приложение: {0}, Устройство: {1}",
"Application": "Приложение",
"Artists": "Исполнители",
"AuthenticationSucceededWithUserName": "{0} - авторизация успешна",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlaybackStopped": "Воспроизведение аудио остановлено",
"NotificationOptionCameraImageUploaded": "Изображения с камеры загружены",
"NotificationOptionInstallationFailed": "Сбой установки",
- "NotificationOptionNewLibraryContent": "Новое содержание добавлено",
+ "NotificationOptionNewLibraryContent": "Новое содержимое добавлено",
"NotificationOptionPluginError": "Сбой плагина",
"NotificationOptionPluginInstalled": "Плагин установлен",
"NotificationOptionPluginUninstalled": "Плагин удалён",
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 1be8867f4..9739358df 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.",
"External": "Спољно",
"TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.",
- "TaskKeyframeExtractor": "Екстрактор кључних сличица"
+ "TaskKeyframeExtractor": "Екстрактор кључних сличица",
+ "HearingImpaired": "ослабљен слух"
}
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 9407a7b92..1a4fef64e 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -120,5 +120,6 @@
"Forced": "บังคับใช้",
"TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล",
"TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น",
- "External": "ภายนอก"
+ "External": "ภายนอก",
+ "HearingImpaired": "บกพร่องทางการได้ยิน"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index baa9ecc1c..cdc25ec7c 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -9,15 +9,15 @@
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
"Collections": "合輯",
- "DeviceOfflineWithName": "{0} 已經斷開連結",
+ "DeviceOfflineWithName": "{0} 已經斷開連接",
"DeviceOnlineWithName": "{0} 已經連接",
- "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗",
+ "FailedLoginAttemptWithUserName": "{0} 登入失敗",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯藝人",
"HeaderContinueWatching": "繼續觀看",
- "HeaderFavoriteAlbums": "最愛專輯",
+ "HeaderFavoriteAlbums": "最愛的專輯",
"HeaderFavoriteArtists": "最愛的藝人",
"HeaderFavoriteEpisodes": "最愛的劇集",
"HeaderFavoriteShows": "最愛的節目",
@@ -44,10 +44,10 @@
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
"NewVersionIsAvailable": "新版本的 Jellyfin 伺服器可供下載。",
- "NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
+ "NotificationOptionApplicationUpdateAvailable": "有可用的更新",
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
- "NotificationOptionAudioPlayback": "開始播放音頻",
- "NotificationOptionAudioPlaybackStopped": "已停止播放音頻",
+ "NotificationOptionAudioPlayback": "開始播放音訊",
+ "NotificationOptionAudioPlaybackStopped": "已停止播放音訊",
"NotificationOptionCameraImageUploaded": "相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已添加新内容",
diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt
index 66fba3330..b55c0fa33 100644
--- a/Emby.Server.Implementations/Localization/iso6392.txt
+++ b/Emby.Server.Implementations/Localization/iso6392.txt
@@ -77,6 +77,7 @@ chb|||Chibcha|chibcha
che||ce|Chechen|tchétchène
chg|||Chagatai|djaghataï
chi|zho|zh|Chinese|chinois
+chi|zho|ze|Chinese; Bilingual|chinois
chi|zho|zh-tw|Chinese; Traditional|chinois
chi|zho|zh-hk|Chinese; Hong Kong|chinois
chk|||Chuukese|chuuk
diff --git a/Emby.Server.Implementations/Plugins/PluginLoadContext.cs b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs
new file mode 100644
index 000000000..d04e9cf68
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/PluginLoadContext.cs
@@ -0,0 +1,33 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Emby.Server.Implementations.Plugins;
+
+/// <summary>
+/// A custom <see cref="AssemblyLoadContext"/> for loading Jellyfin plugins.
+/// </summary>
+public class PluginLoadContext : AssemblyLoadContext
+{
+ private readonly AssemblyDependencyResolver _resolver;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginLoadContext"/> class.
+ /// </summary>
+ /// <param name="path">The path of the plugin assembly.</param>
+ public PluginLoadContext(string path) : base(true)
+ {
+ _resolver = new AssemblyDependencyResolver(path);
+ }
+
+ /// <inheritdoc />
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
+ if (assemblyPath is not null)
+ {
+ return LoadFromAssemblyPath(assemblyPath);
+ }
+
+ return null;
+ }
+}
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 14e7c2269..f2212f4dc 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
+using System.Runtime.Loader;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
@@ -30,6 +31,7 @@ namespace Emby.Server.Implementations.Plugins
{
private readonly string _pluginsPath;
private readonly Version _appVersion;
+ private readonly List<AssemblyLoadContext> _assemblyLoadContexts;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger<PluginManager> _logger;
private readonly IApplicationHost _appHost;
@@ -76,6 +78,8 @@ namespace Emby.Server.Implementations.Plugins
_appHost = appHost;
_minimumVersion = new Version(0, 0, 0, 1);
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
+
+ _assemblyLoadContexts = new List<AssemblyLoadContext>();
}
private IHttpClientFactory HttpClientFactory
@@ -124,7 +128,10 @@ namespace Emby.Server.Implementations.Plugins
Assembly assembly;
try
{
- assembly = Assembly.LoadFrom(file);
+ var assemblyLoadContext = new PluginLoadContext(file);
+ _assemblyLoadContexts.Add(assemblyLoadContext);
+
+ assembly = assemblyLoadContext.LoadFromAssemblyPath(file);
// Load all required types to verify that the plugin will load
assembly.GetTypes();
@@ -156,6 +163,15 @@ namespace Emby.Server.Implementations.Plugins
}
}
+ /// <inheritdoc />
+ public void UnloadAssemblies()
+ {
+ foreach (var assemblyLoadContext in _assemblyLoadContexts)
+ {
+ assemblyLoadContext.Unload();
+ }
+ }
+
/// <summary>
/// Creates all the plugin instances.
/// </summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 1efacd856..1f3cb9b63 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
private readonly ILogger<OptimizeDatabaseTask> _logger;
private readonly ILocalizationManager _localization;
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
/// <summary>
/// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public OptimizeDatabaseTask(
ILogger<OptimizeDatabaseTask> logger,
ILocalizationManager localization,
- IDbContextFactory<JellyfinDb> provider)
+ IDbContextFactory<JellyfinDbContext> provider)
{
_logger = logger;
_localization = localization;
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 2f60d01a9..afa3721b8 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -95,12 +95,6 @@ namespace Emby.Server.Implementations.Session
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
}
- /// <inheritdoc />
- public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
-
- /// <inheritdoc />
- public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
-
/// <summary>
/// Occurs when playback has started.
/// </summary>
@@ -1468,7 +1462,7 @@ namespace Emby.Server.Implementations.Session
if (user is null)
{
- AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request));
+ await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationRequest>(request)).ConfigureAwait(false);
throw new AuthenticationException("Invalid username or password entered.");
}
@@ -1504,8 +1498,7 @@ namespace Emby.Server.Implementations.Session
ServerId = _appHost.SystemId
};
- AuthenticationSucceeded?.Invoke(this, new GenericEventArgs<AuthenticationResult>(returnResult));
-
+ await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationResult>(returnResult)).ConfigureAwait(false);
return returnResult;
}
diff --git a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
index 58552d847..fbe68b6b9 100644
--- a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
@@ -25,6 +25,6 @@ namespace Jellyfin.Api.Attributes
/// Gets the configured content types.
/// </summary>
/// <returns>the configured content types.</returns>
- public string[] GetContentTypes() => _contentTypes;
+ public string[] ContentTypes => _contentTypes;
}
}
diff --git a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
index 2bf77d729..d8e4141ac 100644
--- a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
@@ -25,6 +25,6 @@ namespace Jellyfin.Api.Attributes
/// Gets the configured content types.
/// </summary>
/// <returns>the configured content types.</returns>
- public string[] GetContentTypes() => _contentTypes;
+ public string[] ContentTypes => _contentTypes;
}
}
diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index 0c63d24b7..e327831fe 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -23,24 +23,6 @@ namespace Jellyfin.Api
/// <param name="value">The value to return.</param>
/// <typeparam name="T">The type to return.</typeparam>
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
- protected ActionResult<IEnumerable<T>> Ok<T>(List<T> value)
- => new OkResult<IEnumerable<T>>(value);
-
- /// <summary>
- /// Create a new <see cref="OkResult{T}"/>.
- /// </summary>
- /// <param name="value">The value to return.</param>
- /// <typeparam name="T">The type to return.</typeparam>
- /// <returns>The <see cref="ActionResult{T}"/>.</returns>
- protected ActionResult<IEnumerable<T>> Ok<T>(IReadOnlyList<T> value)
- => new OkResult<IEnumerable<T>>(value);
-
- /// <summary>
- /// Create a new <see cref="OkResult{T}"/>.
- /// </summary>
- /// <param name="value">The value to return.</param>
- /// <typeparam name="T">The type to return.</typeparam>
- /// <returns>The <see cref="ActionResult{T}"/>.</returns>
protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value)
=> new OkResult<IEnumerable<T>?>(value);
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index 593846adc..024a15349 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
{
- var keys = await _authenticationManager.GetApiKeys();
+ var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
return new QueryResult<AuthenticationInfo>(keys);
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index af43bb578..ba9a57f1d 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
@@ -1704,11 +1705,12 @@ namespace Jellyfin.Api.Controllers
return audioTranscodeParams;
}
- // flac and opus are experimental in mp4 muxer
+ // dts, flac and opus are experimental in mp4 muxer
var strictArgs = string.Empty;
if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase))
{
strictArgs = " -strict -2";
}
@@ -1731,7 +1733,12 @@ namespace Jellyfin.Api.Controllers
var channels = state.OutputAudioChannels;
- if (channels.HasValue)
+ if (channels.HasValue
+ && (channels.Value != 2
+ || (state.AudioStream is not null
+ && state.AudioStream.Channels.HasValue
+ && state.AudioStream.Channels.Value > 5
+ && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)))
{
args += " -ac " + channels.Value;
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 49342ad5c..f866655c0 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -28,7 +28,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Api.Controllers
@@ -106,24 +105,26 @@ namespace Jellyfin.Api.Controllers
}
var user = _userManager.GetUserById(userId);
- await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
- if (user.ProfileImage is not null)
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
{
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
- }
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ if (user.ProfileImage is not null)
+ {
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ }
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
- await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
- .ConfigureAwait(false);
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ await _providerManager
+ .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .ConfigureAwait(false);
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
- return NoContent();
+ return NoContent();
+ }
}
/// <summary>
@@ -153,24 +154,26 @@ namespace Jellyfin.Api.Controllers
}
var user = _userManager.GetUserById(userId);
- await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
- if (user.ProfileImage is not null)
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
{
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
- }
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ if (user.ProfileImage is not null)
+ {
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ }
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
- await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
- .ConfigureAwait(false);
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ await _providerManager
+ .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .ConfigureAwait(false);
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
- return NoContent();
+ return NoContent();
+ }
}
/// <summary>
@@ -341,14 +344,16 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
- await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
+ return NoContent();
+ }
}
/// <summary>
@@ -377,14 +382,16 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
- await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
+ return NoContent();
+ }
}
/// <summary>
@@ -1788,32 +1795,35 @@ namespace Jellyfin.Api.Controllers
[AcceptsImageFile]
public async Task<ActionResult> UploadCustomSplashscreen()
{
- await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType;
- var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType;
+ if (!mimeType.HasValue)
+ {
+ return BadRequest("Error reading mimetype from uploaded image");
+ }
- if (!mimeType.HasValue)
- {
- return BadRequest("Error reading mimetype from uploaded image");
- }
+ var extension = MimeTypes.ToExtension(mimeType.Value);
+ if (string.IsNullOrEmpty(extension))
+ {
+ return BadRequest("Error converting mimetype to an image extension");
+ }
- var extension = MimeTypes.ToExtension(mimeType.Value);
- if (string.IsNullOrEmpty(extension))
- {
- return BadRequest("Error converting mimetype to an image extension");
- }
+ var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
+ var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ brandingOptions.SplashscreenLocation = filePath;
+ _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
- var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
- var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- brandingOptions.SplashscreenLocation = filePath;
- _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
+ var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await using (fs.ConfigureAwait(false))
+ {
+ await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
+ }
- await using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
- {
- await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
}
-
- return NoContent();
}
/// <summary>
@@ -2027,13 +2037,8 @@ namespace Jellyfin.Api.Controllers
}
var acceptParam = Request.Query[HeaderNames.Accept];
- if (StringValues.IsNullOrEmpty(acceptParam))
- {
- return Array.Empty<ImageFormat>();
- }
- // Can't be null, checked above
- var supportsWebP = SupportsFormat(supportedFormats, acceptParam!, ImageFormat.Webp, false);
+ var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
if (!supportsWebP)
{
@@ -2055,8 +2060,7 @@ namespace Jellyfin.Api.Controllers
formats.Add(ImageFormat.Jpg);
formats.Add(ImageFormat.Png);
- // Can't be null, checked above
- if (SupportsFormat(supportedFormats, acceptParam!, ImageFormat.Gif, true))
+ if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
{
formats.Add(ImageFormat.Gif);
}
@@ -2064,7 +2068,7 @@ namespace Jellyfin.Api.Controllers
return formats.ToArray();
}
- private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll)
+ private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string? acceptParam, ImageFormat format, bool acceptAll)
{
if (requestAcceptTypes.Contains(format.GetMimeType()))
{
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 94710d78f..5228e0bab 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1011,10 +1011,9 @@ namespace Jellyfin.Api.Controllers
{
if (!string.IsNullOrEmpty(pw))
{
- using var sha = SHA1.Create();
// TODO: remove ToLower when Convert.ToHexString supports lowercase
// Schedules Direct requires the hex to be lowercase
- listingsProviderInfo.Password = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
+ listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
}
return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 420630cdf..a28556476 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,12 +1,5 @@
-using System;
using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.Linq;
-using System.Threading;
using Jellyfin.Api.Constants;
-using Jellyfin.Api.Models.NotificationDtos;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Notifications;
@@ -23,41 +16,14 @@ namespace Jellyfin.Api.Controllers
public class NotificationsController : BaseJellyfinApiController
{
private readonly INotificationManager _notificationManager;
- private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="NotificationsController" /> class.
/// </summary>
/// <param name="notificationManager">The notification manager.</param>
- /// <param name="userManager">The user manager.</param>
- public NotificationsController(INotificationManager notificationManager, IUserManager userManager)
+ public NotificationsController(INotificationManager notificationManager)
{
_notificationManager = notificationManager;
- _userManager = userManager;
- }
-
- /// <summary>
- /// Gets a user's notifications.
- /// </summary>
- /// <response code="200">Notifications returned.</response>
- /// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
- [HttpGet("{userId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<NotificationResultDto> GetNotifications()
- {
- return new NotificationResultDto();
- }
-
- /// <summary>
- /// Gets a user's notification summary.
- /// </summary>
- /// <response code="200">Summary of user's notifications returned.</response>
- /// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns>
- [HttpGet("{userId}/Summary")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<NotificationsSummaryDto> GetNotificationsSummary()
- {
- return new NotificationsSummaryDto();
}
/// <summary>
@@ -83,56 +49,5 @@ namespace Jellyfin.Api.Controllers
{
return _notificationManager.GetNotificationServices();
}
-
- /// <summary>
- /// Sends a notification to all admins.
- /// </summary>
- /// <param name="notificationDto">The notification request.</param>
- /// <response code="204">Notification sent.</response>
- /// <returns>A <cref see="NoContentResult"/>.</returns>
- [HttpPost("Admin")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto)
- {
- var notification = new NotificationRequest
- {
- Name = notificationDto.Name,
- Description = notificationDto.Description,
- Url = notificationDto.Url,
- Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal,
- UserIds = _userManager.Users
- .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
- .Select(user => user.Id)
- .ToArray(),
- Date = DateTime.UtcNow,
- };
-
- _notificationManager.SendNotification(notification, CancellationToken.None);
- return NoContent();
- }
-
- /// <summary>
- /// Sets notifications as read.
- /// </summary>
- /// <response code="204">Notifications set as read.</response>
- /// <returns>A <cref see="NoContentResult"/>.</returns>
- [HttpPost("{userId}/Read")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SetRead()
- {
- return NoContent();
- }
-
- /// <summary>
- /// Sets notifications as unread.
- /// </summary>
- /// <response code="204">Notifications set as unread.</response>
- /// <returns>A <cref see="NoContentResult"/>.</returns>
- [HttpPost("{userId}/Unread")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SetUnread()
- {
- return NoContent();
- }
}
}
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 0aa7c2ac9..10f967dcd 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -145,7 +145,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
{
- return _serverConfigurationManager.Configuration.PluginRepositories;
+ return Ok(_serverConfigurationManager.Configuration.PluginRepositories.AsEnumerable());
}
/// <summary>
@@ -157,7 +157,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Repositories")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
+ public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos)
{
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
_serverConfigurationManager.SaveConfiguration();
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 6a729b237..b8a09990a 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
@@ -143,7 +144,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
{
// If no version is given, return the current instance.
- var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId));
+ var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList();
// Select the un-instanced one first.
var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index ff9bd095b..c3ce1868e 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -236,14 +236,17 @@ namespace Jellyfin.Api.Controllers
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
{
- await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
- using var reader = new StreamReader(stream);
+ Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
+ {
+ using var reader = new StreamReader(stream);
- var text = await reader.ReadToEndAsync().ConfigureAwait(false);
+ var text = await reader.ReadToEndAsync().ConfigureAwait(false);
- text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal);
+ text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal);
- return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format));
+ return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format));
+ }
}
return File(
@@ -403,19 +406,22 @@ namespace Jellyfin.Api.Controllers
{
var video = (Video)_libraryManager.GetItemById(itemId);
var data = Convert.FromBase64String(body.Data);
- await using var memoryStream = new MemoryStream(data);
- await _subtitleManager.UploadSubtitle(
- video,
- new SubtitleResponse
- {
- Format = body.Format,
- Language = body.Language,
- IsForced = body.IsForced,
- Stream = memoryStream
- }).ConfigureAwait(false);
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ await _subtitleManager.UploadSubtitle(
+ video,
+ new SubtitleResponse
+ {
+ Format = body.Format,
+ Language = body.Language,
+ IsForced = body.IsForced,
+ Stream = memoryStream
+ }).ConfigureAwait(false);
+ _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
- return NoContent();
+ return NoContent();
+ }
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index e194fc556..99347246e 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@@ -107,7 +108,7 @@ namespace Jellyfin.Api.Controllers
{
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var syncPlayRequest = new ListGroupsRequest();
- return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest));
+ return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable());
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 411c987f3..2d594293e 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -216,8 +216,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
{
var result = _network.GetMacAddresses()
- .Select(i => new WakeOnLanInfo(i))
- .ToList();
+ .Select(i => new WakeOnLanInfo(i));
return Ok(result);
}
}
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index c18fa29af..cd21c5f6f 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -211,7 +211,7 @@ namespace Jellyfin.Api.Controllers
if (item is IHasTrailers hasTrailers)
{
var trailers = hasTrailers.LocalTrailers;
- return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item));
+ return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
}
return Ok(item.GetExtras()
diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs
index ea8c5ecdb..8f1f5dd94 100644
--- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Api/Formatters/CamelCaseJsonProfileFormatter.cs
@@ -2,7 +2,7 @@ using Jellyfin.Extensions.Json;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Server.Formatters
+namespace Jellyfin.Api.Formatters
{
/// <summary>
/// Camel Case Json Profile Formatter.
diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Api/Formatters/CssOutputFormatter.cs
index fdaa48f84..e88c8ad1b 100644
--- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs
+++ b/Jellyfin.Api/Formatters/CssOutputFormatter.cs
@@ -3,7 +3,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
-namespace Jellyfin.Server.Formatters
+namespace Jellyfin.Api.Formatters
{
/// <summary>
/// Css output formatter.
diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs
index 03ca7dda7..5d77dbf4c 100644
--- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Api/Formatters/PascalCaseJsonProfileFormatter.cs
@@ -3,7 +3,7 @@ using Jellyfin.Extensions.Json;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Server.Formatters
+namespace Jellyfin.Api.Formatters
{
/// <summary>
/// Pascal Case Json Profile Formatter.
diff --git a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
index 156368d69..df8b1650b 100644
--- a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs
+++ b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
@@ -4,7 +4,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
-namespace Jellyfin.Server.Formatters
+namespace Jellyfin.Api.Formatters
{
/// <summary>
/// Xml output formatter.
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index e8ce1ca2a..e0245fe4d 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -181,7 +181,7 @@ namespace Jellyfin.Api.Helpers
{
var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
- var options = new VideoOptions
+ var options = new MediaOptions
{
MediaSources = new[] { mediaSource },
Context = EncodingContext.Streaming,
@@ -244,8 +244,8 @@ namespace Jellyfin.Api.Helpers
// Beginning of Playback Determination
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
+ ? streamBuilder.GetOptimalAudioStream(options)
+ : streamBuilder.GetOptimalVideoStream(options);
if (streamInfo is not null)
{
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 889f7dc9a..45725ec3e 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -12,12 +12,8 @@
<NoWarn>AD0001</NoWarn>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
- </PropertyGroup>
-
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
@@ -31,7 +27,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs
index 6ee5bf38a..6bd9e0b08 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Redirect requests without baseurl prefix to the baseurl prefixed URL.
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
index 91dbce19a..6b3aeb187 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
@@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Exception Middleware.
diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs
index 0afcd61a0..f7af91e48 100644
--- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
+++ b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs
@@ -4,7 +4,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Http;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Validates the IP of requests coming from local networks wrt. remote access.
diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
index 67bf24d2a..18f13bbce 100644
--- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
+++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Validates the LAN host IP based on application configuration.
diff --git a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
index b214299df..b73923c1e 100644
--- a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
+++ b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
@@ -3,7 +3,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Removes /emby and /mediabrowser from requested route.
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs
index 24807ce38..4b6304e0e 100644
--- a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
+++ b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs
@@ -2,7 +2,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// URL decodes the querystring before binding.
diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs
index 531897cd4..3701d0f45 100644
--- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
+++ b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Response time middleware.
diff --git a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
index fabcd2da7..2e69580be 100644
--- a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
@@ -3,7 +3,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Redirect requests to robots.txt to web/robots.txt.
diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs
index 2ec063392..dcd64401a 100644
--- a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
+++ b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Controller;
using MediaBrowser.Model.Globalization;
using Microsoft.AspNetCore.Http;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Shows a custom message during server startup.
diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs
index 2f1d79157..d35e0fcfd 100644
--- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
+++ b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Defines the <see cref="UrlDecodeQueryFeature"/>.
diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs
index b7a5d2b34..2cf1e5e4a 100644
--- a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
+++ b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs
@@ -2,7 +2,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
-namespace Jellyfin.Server.Middleware
+namespace Jellyfin.Api.Middleware
{
/// <summary>
/// Handles WebSocket requests.
diff --git a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
index f43822da7..e293c461c 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
@@ -14,14 +14,12 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// <summary>
/// Gets or sets list of tuner channels.
/// </summary>
- [SuppressMessage("Microsoft.Performance", "CA2227:ReadOnlyRemoveSetter", MessageId = "TunerChannels", Justification = "Imported from ServiceStack")]
- public List<TunerChannelMapping> TunerChannels { get; set; } = null!;
+ required public IReadOnlyList<TunerChannelMapping> TunerChannels { get; set; }
/// <summary>
/// Gets or sets list of provider channels.
/// </summary>
- [SuppressMessage("Microsoft.Performance", "CA2227:ReadOnlyRemoveSetter", MessageId = "ProviderChannels", Justification = "Imported from ServiceStack")]
- public List<NameIdPair> ProviderChannels { get; set; } = null!;
+ required public IReadOnlyList<NameIdPair> ProviderChannels { get; set; }
/// <summary>
/// Gets or sets list of mappings.
diff --git a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs
deleted file mode 100644
index 2c3a6282f..000000000
--- a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using MediaBrowser.Model.Notifications;
-
-namespace Jellyfin.Api.Models.NotificationDtos
-{
- /// <summary>
- /// The admin notification dto.
- /// </summary>
- public class AdminNotificationDto
- {
- /// <summary>
- /// Gets or sets the notification name.
- /// </summary>
- public string? Name { get; set; }
-
- /// <summary>
- /// Gets or sets the notification description.
- /// </summary>
- public string? Description { get; set; }
-
- /// <summary>
- /// Gets or sets the notification level.
- /// </summary>
- public NotificationLevel? NotificationLevel { get; set; }
-
- /// <summary>
- /// Gets or sets the notification url.
- /// </summary>
- public string? Url { get; set; }
- }
-}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
deleted file mode 100644
index af5239ec2..000000000
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using MediaBrowser.Model.Notifications;
-
-namespace Jellyfin.Api.Models.NotificationDtos
-{
- /// <summary>
- /// The notification DTO.
- /// </summary>
- public class NotificationDto
- {
- /// <summary>
- /// Gets or sets the notification ID. Defaults to an empty string.
- /// </summary>
- public string Id { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification's user ID. Defaults to an empty string.
- /// </summary>
- public string UserId { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification date.
- /// </summary>
- public DateTime Date { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether the notification has been read. Defaults to false.
- /// </summary>
- public bool IsRead { get; set; } = false;
-
- /// <summary>
- /// Gets or sets the notification's name. Defaults to an empty string.
- /// </summary>
- public string Name { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification's description. Defaults to an empty string.
- /// </summary>
- public string Description { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification's URL. Defaults to an empty string.
- /// </summary>
- public string Url { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the notification level.
- /// </summary>
- public NotificationLevel Level { get; set; }
- }
-}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
deleted file mode 100644
index 64e92bd83..000000000
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Jellyfin.Api.Models.NotificationDtos
-{
- /// <summary>
- /// A list of notifications with the total record count for pagination.
- /// </summary>
- public class NotificationResultDto
- {
- /// <summary>
- /// Gets or sets the current page of notifications.
- /// </summary>
- public IReadOnlyList<NotificationDto> Notifications { get; set; } = Array.Empty<NotificationDto>();
-
- /// <summary>
- /// Gets or sets the total number of notifications.
- /// </summary>
- public int TotalRecordCount { get; set; }
- }
-}
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
deleted file mode 100644
index 0568dea66..000000000
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using MediaBrowser.Model.Notifications;
-
-namespace Jellyfin.Api.Models.NotificationDtos
-{
- /// <summary>
- /// The notification summary DTO.
- /// </summary>
- public class NotificationsSummaryDto
- {
- /// <summary>
- /// Gets or sets the number of unread notifications.
- /// </summary>
- public int UnreadCount { get; set; }
-
- /// <summary>
- /// Gets or sets the maximum unread notification level.
- /// </summary>
- public NotificationLevel? MaxUnreadNotificationLevel { get; set; }
- }
-}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 7fe6466d4..540534e1b 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -29,7 +29,7 @@
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
deleted file mode 100644
index 6136a2ff9..000000000
--- a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System;
-using MediaBrowser.Model.Drawing;
-using SkiaSharp;
-
-namespace Jellyfin.Drawing.Skia
-{
- /// <summary>
- /// Static helper class used to draw percentage-played indicators on images.
- /// </summary>
- public static class PercentPlayedDrawer
- {
- private const int IndicatorHeight = 8;
-
- /// <summary>
- /// Draw a percentage played indicator on a canvas.
- /// </summary>
- /// <param name="canvas">The canvas to draw the indicator on.</param>
- /// <param name="imageSize">The size of the image being drawn on.</param>
- /// <param name="percent">The percentage played to display with the indicator.</param>
- public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
- {
- using var paint = new SKPaint();
- var endX = imageSize.Width - 1;
- var endY = imageSize.Height - 1;
-
- paint.Color = SKColor.Parse("#99000000");
- paint.Style = SKPaintStyle.Fill;
- canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
-
- double foregroundWidth = (endX * percent) / 100;
-
- paint.Color = SKColor.Parse("#FF00A4DC");
- canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
- }
- }
-}
diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
deleted file mode 100644
index 2a3729942..000000000
--- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using MediaBrowser.Model.Drawing;
-using SkiaSharp;
-
-namespace Jellyfin.Drawing.Skia
-{
- /// <summary>
- /// Static helper class for drawing 'played' indicators.
- /// </summary>
- public static class PlayedIndicatorDrawer
- {
- private const int OffsetFromTopRightCorner = 38;
-
- /// <summary>
- /// Draw a 'played' indicator in the top right corner of a canvas.
- /// </summary>
- /// <param name="canvas">The canvas to draw the indicator on.</param>
- /// <param name="imageSize">
- /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
- /// indicator.
- /// </param>
- public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize)
- {
- var x = imageSize.Width - OffsetFromTopRightCorner;
-
- using var paint = new SKPaint
- {
- Color = SKColor.Parse("#CC00A4DC"),
- Style = SKPaintStyle.Fill
- };
-
- canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
-
- paint.Color = new SKColor(255, 255, 255, 255);
- paint.TextSize = 30;
- paint.IsAntialias = true;
-
- // or:
- // var emojiChar = 0x1F680;
- const string Text = "✔️";
- var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
-
- // ask the font manager for a font with that character
- paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
-
- canvas.DrawText(Text, (float)x - 12, OffsetFromTopRightCorner + 12, paint);
- }
- }
-}
diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/Jellyfin.Drawing.Skia/SkiaCodecException.cs
deleted file mode 100644
index 9a50a4d62..000000000
--- a/Jellyfin.Drawing.Skia/SkiaCodecException.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Globalization;
-using SkiaSharp;
-
-namespace Jellyfin.Drawing.Skia
-{
- /// <summary>
- /// Represents errors that occur during interaction with Skia codecs.
- /// </summary>
- public class SkiaCodecException : SkiaException
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
- /// </summary>
- /// <param name="result">The non-successful codec result returned by Skia.</param>
- public SkiaCodecException(SKCodecResult result)
- {
- CodecResult = result;
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaCodecException" /> class
- /// with a specified error message.
- /// </summary>
- /// <param name="result">The non-successful codec result returned by Skia.</param>
- /// <param name="message">The message that describes the error.</param>
- public SkiaCodecException(SKCodecResult result, string message)
- : base(message)
- {
- CodecResult = result;
- }
-
- /// <summary>
- /// Gets the non-successful codec result returned by Skia.
- /// </summary>
- public SKCodecResult CodecResult { get; }
-
- /// <inheritdoc />
- public override string ToString()
- => string.Format(
- CultureInfo.InvariantCulture,
- "Non-success codec result: {0}\n{1}",
- CodecResult,
- base.ToString());
- }
-}
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
deleted file mode 100644
index 9171c4d6e..000000000
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ /dev/null
@@ -1,545 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using BlurHashSharp.SkiaSharp;
-using Jellyfin.Extensions;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Model.Drawing;
-using Microsoft.Extensions.Logging;
-using SkiaSharp;
-using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
-
-namespace Jellyfin.Drawing.Skia
-{
- /// <summary>
- /// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
- /// </summary>
- public class SkiaEncoder : IImageEncoder
- {
- private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
-
- private readonly ILogger<SkiaEncoder> _logger;
- private readonly IApplicationPaths _appPaths;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
- /// </summary>
- /// <param name="logger">The application logger.</param>
- /// <param name="appPaths">The application paths.</param>
- public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
- {
- _logger = logger;
- _appPaths = appPaths;
- }
-
- /// <inheritdoc/>
- public string Name => "Skia";
-
- /// <inheritdoc/>
- public bool SupportsImageCollageCreation => true;
-
- /// <inheritdoc/>
- public bool SupportsImageEncoding => true;
-
- /// <inheritdoc/>
- public IReadOnlyCollection<string> SupportedInputFormats =>
- new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "jpeg",
- "jpg",
- "png",
- "dng",
- "webp",
- "gif",
- "bmp",
- "ico",
- "astc",
- "ktx",
- "pkm",
- "wbmp",
- // TODO: check if these are supported on multiple platforms
- // https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
- // working on windows at least
- "cr2",
- "nef",
- "arw"
- };
-
- /// <inheritdoc/>
- public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
- => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
-
- /// <summary>
- /// Check if the native lib is available.
- /// </summary>
- /// <returns>True if the native lib is available, otherwise false.</returns>
- public static bool IsNativeLibAvailable()
- {
- try
- {
- // test an operation that requires the native library
- SKPMColor.PreMultiply(SKColors.Black);
- return true;
- }
- catch (Exception)
- {
- return false;
- }
- }
-
- /// <summary>
- /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
- /// </summary>
- /// <param name="selectedFormat">The format to convert.</param>
- /// <returns>The converted format.</returns>
- public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
- {
- return selectedFormat switch
- {
- ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
- ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
- ImageFormat.Gif => SKEncodedImageFormat.Gif,
- ImageFormat.Webp => SKEncodedImageFormat.Webp,
- _ => SKEncodedImageFormat.Png
- };
- }
-
- /// <inheritdoc />
- /// <exception cref="FileNotFoundException">The path is not valid.</exception>
- public ImageDimensions GetImageSize(string path)
- {
- if (!File.Exists(path))
- {
- throw new FileNotFoundException("File not found", path);
- }
-
- var extension = Path.GetExtension(path.AsSpan());
- if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
- {
- var svg = new SKSvg();
- svg.Load(path);
- return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
- }
-
- using var codec = SKCodec.Create(path, out SKCodecResult result);
- switch (result)
- {
- case SKCodecResult.Success:
- var info = codec.Info;
- return new ImageDimensions(info.Width, info.Height);
- case SKCodecResult.Unimplemented:
- _logger.LogDebug("Image format not supported: {FilePath}", path);
- return new ImageDimensions(0, 0);
- default:
- _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
- return new ImageDimensions(0, 0);
- }
- }
-
- /// <inheritdoc />
- /// <exception cref="ArgumentNullException">The path is null.</exception>
- /// <exception cref="FileNotFoundException">The path is not valid.</exception>
- /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
- public string GetImageBlurHash(int xComp, int yComp, string path)
- {
- ArgumentException.ThrowIfNullOrEmpty(path);
-
- var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
- if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
- return string.Empty;
- }
-
- // Any larger than 128x128 is too slow and there's no visually discernible difference
- return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
- }
-
- private bool RequiresSpecialCharacterHack(string path)
- {
- for (int i = 0; i < path.Length; i++)
- {
- if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
- {
- return true;
- }
- }
-
- return path.HasDiacritics();
- }
-
- private string NormalizePath(string path)
- {
- if (!RequiresSpecialCharacterHack(path))
- {
- return path;
- }
-
- var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
- var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
- Directory.CreateDirectory(directory);
- File.Copy(path, tempPath, true);
-
- return tempPath;
- }
-
- private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
- {
- if (!orientation.HasValue)
- {
- return SKEncodedOrigin.TopLeft;
- }
-
- return orientation.Value switch
- {
- ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
- ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
- ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
- ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
- ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
- ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
- ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
- _ => SKEncodedOrigin.TopLeft
- };
- }
-
- /// <summary>
- /// Decode an image.
- /// </summary>
- /// <param name="path">The filepath of the image to decode.</param>
- /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
- /// <param name="orientation">The orientation of the image.</param>
- /// <param name="origin">The detected origin of the image.</param>
- /// <returns>The resulting bitmap of the image.</returns>
- internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
- {
- if (!File.Exists(path))
- {
- throw new FileNotFoundException("File not found", path);
- }
-
- var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
-
- if (requiresTransparencyHack || forceCleanBitmap)
- {
- using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
- if (res != SKCodecResult.Success)
- {
- origin = GetSKEncodedOrigin(orientation);
- return null;
- }
-
- // create the bitmap
- var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
-
- // decode
- _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
-
- origin = codec.EncodedOrigin;
-
- return bitmap;
- }
-
- var resultBitmap = SKBitmap.Decode(NormalizePath(path));
-
- if (resultBitmap is null)
- {
- return Decode(path, true, orientation, out origin);
- }
-
- // If we have to resize these they often end up distorted
- if (resultBitmap.ColorType == SKColorType.Gray8)
- {
- using (resultBitmap)
- {
- return Decode(path, true, orientation, out origin);
- }
- }
-
- origin = SKEncodedOrigin.TopLeft;
- return resultBitmap;
- }
-
- private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
- {
- if (autoOrient)
- {
- var bitmap = Decode(path, true, orientation, out var origin);
-
- if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
- {
- using (bitmap)
- {
- return OrientImage(bitmap, origin);
- }
- }
-
- return bitmap;
- }
-
- return Decode(path, false, orientation, out _);
- }
-
- private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
- {
- var needsFlip = origin == SKEncodedOrigin.LeftBottom
- || origin == SKEncodedOrigin.LeftTop
- || origin == SKEncodedOrigin.RightBottom
- || origin == SKEncodedOrigin.RightTop;
- var rotated = needsFlip
- ? new SKBitmap(bitmap.Height, bitmap.Width)
- : new SKBitmap(bitmap.Width, bitmap.Height);
- using var surface = new SKCanvas(rotated);
- var midX = (float)rotated.Width / 2;
- var midY = (float)rotated.Height / 2;
-
- switch (origin)
- {
- case SKEncodedOrigin.TopRight:
- surface.Scale(-1, 1, midX, midY);
- break;
- case SKEncodedOrigin.BottomRight:
- surface.RotateDegrees(180, midX, midY);
- break;
- case SKEncodedOrigin.BottomLeft:
- surface.Scale(1, -1, midX, midY);
- break;
- case SKEncodedOrigin.LeftTop:
- surface.Translate(0, -rotated.Height);
- surface.Scale(1, -1, midX, midY);
- surface.RotateDegrees(-90);
- break;
- case SKEncodedOrigin.RightTop:
- surface.Translate(rotated.Width, 0);
- surface.RotateDegrees(90);
- break;
- case SKEncodedOrigin.RightBottom:
- surface.Translate(rotated.Width, 0);
- surface.Scale(1, -1, midX, midY);
- surface.RotateDegrees(90);
- break;
- case SKEncodedOrigin.LeftBottom:
- surface.Translate(0, rotated.Height);
- surface.RotateDegrees(-90);
- break;
- }
-
- surface.DrawBitmap(bitmap, 0, 0);
- return rotated;
- }
-
- /// <summary>
- /// Resizes an image on the CPU, by utilizing a surface and canvas.
- ///
- /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
- /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html).
- /// </summary>
- /// <param name="source">The source bitmap.</param>
- /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
- /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
- /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
- /// <returns>The resized image.</returns>
- internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
- {
- using var surface = SKSurface.Create(targetInfo);
- using var canvas = surface.Canvas;
- using var paint = new SKPaint
- {
- FilterQuality = SKFilterQuality.High,
- IsAntialias = isAntialias,
- IsDither = isDither
- };
-
- var kernel = new float[9]
- {
- 0, -.1f, 0,
- -.1f, 1.4f, -.1f,
- 0, -.1f, 0,
- };
-
- var kernelSize = new SKSizeI(3, 3);
- var kernelOffset = new SKPointI(1, 1);
-
- paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
- kernelSize,
- kernel,
- 1f,
- 0f,
- kernelOffset,
- SKShaderTileMode.Clamp,
- true);
-
- canvas.DrawBitmap(
- source,
- SKRect.Create(0, 0, source.Width, source.Height),
- SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
- paint);
-
- return surface.Snapshot();
- }
-
- /// <inheritdoc/>
- public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
- {
- ArgumentException.ThrowIfNullOrEmpty(inputPath);
- ArgumentException.ThrowIfNullOrEmpty(outputPath);
-
- var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
- if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
- return inputPath;
- }
-
- var skiaOutputFormat = GetImageFormat(outputFormat);
-
- var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
- var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
- var blur = options.Blur ?? 0;
- var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
-
- using var bitmap = GetBitmap(inputPath, autoOrient, orientation);
- if (bitmap is null)
- {
- throw new InvalidDataException($"Skia unable to read image {inputPath}");
- }
-
- var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
-
- if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
- {
- // Just spit out the original file if all the options are default
- return inputPath;
- }
-
- var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
-
- var width = newImageSize.Width;
- var height = newImageSize.Height;
-
- // scale image (the FromImage creates a copy)
- var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
- using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
-
- // If all we're doing is resizing then we can stop now
- if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
- {
- var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
- Directory.CreateDirectory(outputDirectory);
- using var outputStream = new SKFileWStream(outputPath);
- using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
- resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
- return outputPath;
- }
-
- // create bitmap to use for canvas drawing used to draw into bitmap
- using var saveBitmap = new SKBitmap(width, height);
- using var canvas = new SKCanvas(saveBitmap);
- // set background color if present
- if (hasBackgroundColor)
- {
- canvas.Clear(SKColor.Parse(options.BackgroundColor));
- }
-
- // Add blur if option is present
- if (blur > 0)
- {
- // create image from resized bitmap to apply blur
- using var paint = new SKPaint();
- using var filter = SKImageFilter.CreateBlur(blur, blur);
- paint.ImageFilter = filter;
- canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
- }
- else
- {
- // draw resized bitmap onto canvas
- canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
- }
-
- // If foreground layer present then draw
- if (hasForegroundColor)
- {
- if (!double.TryParse(options.ForegroundLayer, out double opacity))
- {
- opacity = .4;
- }
-
- canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
- }
-
- if (hasIndicator)
- {
- DrawIndicator(canvas, width, height, options);
- }
-
- var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
- Directory.CreateDirectory(directory);
- using (var outputStream = new SKFileWStream(outputPath))
- {
- using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
- {
- pixmap.Encode(outputStream, skiaOutputFormat, quality);
- }
- }
-
- return outputPath;
- }
-
- /// <inheritdoc/>
- public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
- {
- double ratio = (double)options.Width / options.Height;
-
- if (ratio >= 1.4)
- {
- new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName);
- }
- else if (ratio >= .9)
- {
- new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
- }
- else
- {
- // TODO: Create Poster collage capability
- new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
- }
- }
-
- /// <inheritdoc />
- public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
- {
- var splashBuilder = new SplashscreenBuilder(this);
- var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
- splashBuilder.GenerateSplash(posters, backdrops, outputPath);
- }
-
- private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
- {
- try
- {
- var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
-
- if (options.AddPlayedIndicator)
- {
- PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize);
- }
- else if (options.UnplayedCount.HasValue)
- {
- UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
- }
-
- if (options.PercentPlayed > 0)
- {
- PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error drawing indicator overlay");
- }
- }
- }
-}
diff --git a/Jellyfin.Drawing.Skia/SkiaException.cs b/Jellyfin.Drawing.Skia/SkiaException.cs
deleted file mode 100644
index 5b272eac5..000000000
--- a/Jellyfin.Drawing.Skia/SkiaException.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System;
-
-namespace Jellyfin.Drawing.Skia
-{
- /// <summary>
- /// Represents errors that occur during interaction with Skia.
- /// </summary>
- public class SkiaException : Exception
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaException"/> class.
- /// </summary>
- public SkiaException()
- {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message.
- /// </summary>
- /// <param name="message">The message that describes the error.</param>
- public SkiaException(string message) : base(message)
- {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message and a
- /// reference to the inner exception that is the cause of this exception.
- /// </summary>
- /// <param name="message">The error message that explains the reason for the exception.</param>
- /// <param name="innerException">
- /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
- /// no inner exception is specified.
- /// </param>
- public SkiaException(string message, Exception innerException)
- : base(message, innerException)
- {
- }
- }
-}
diff --git a/Jellyfin.Drawing.Skia/SkiaHelper.cs b/Jellyfin.Drawing.Skia/SkiaHelper.cs
deleted file mode 100644
index 23e92dcb2..000000000
--- a/Jellyfin.Drawing.Skia/SkiaHelper.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System.Collections.Generic;
-using SkiaSharp;
-
-namespace Jellyfin.Drawing.Skia
-{
- /// <summary>
- /// Class containing helper methods for working with SkiaSharp.
- /// </summary>
- public static class SkiaHelper
- {
- /// <summary>
- /// Gets the next valid image as a bitmap.
- /// </summary>
- /// <param name="skiaEncoder">The current skia encoder.</param>
- /// <param name="paths">The list of image paths.</param>
- /// <param name="currentIndex">The current checked index.</param>
- /// <param name="newIndex">The new index.</param>
- /// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns>
- public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)
- {
- var imagesTested = new Dictionary<int, int>();
- SKBitmap? bitmap = null;
-
- while (imagesTested.Count < paths.Count)
- {
- if (currentIndex >= paths.Count)
- {
- currentIndex = 0;
- }
-
- bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
-
- imagesTested[currentIndex] = 0;
-
- currentIndex++;
-
- if (bitmap is not null)
- {
- break;
- }
- }
-
- newIndex = currentIndex;
- return bitmap;
- }
- }
-}
diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
deleted file mode 100644
index 7fbae3349..000000000
--- a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-using System;
-using System.Collections.Generic;
-using SkiaSharp;
-
-namespace Jellyfin.Drawing.Skia
-{
- /// <summary>
- /// Used to build the splashscreen.
- /// </summary>
- public class SplashscreenBuilder
- {
- private const int FinalWidth = 1920;
- private const int FinalHeight = 1080;
- // generated collage resolution should be higher than the final resolution
- private const int WallWidth = FinalWidth * 3;
- private const int WallHeight = FinalHeight * 2;
- private const int Rows = 6;
- private const int Spacing = 20;
-
- private readonly SkiaEncoder _skiaEncoder;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class.
- /// </summary>
- /// <param name="skiaEncoder">The SkiaEncoder.</param>
- public SplashscreenBuilder(SkiaEncoder skiaEncoder)
- {
- _skiaEncoder = skiaEncoder;
- }
-
- /// <summary>
- /// Generate a splashscreen.
- /// </summary>
- /// <param name="posters">The poster paths.</param>
- /// <param name="backdrops">The landscape paths.</param>
- /// <param name="outputPath">The output path.</param>
- public void GenerateSplash(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops, string outputPath)
- {
- using var wall = GenerateCollage(posters, backdrops);
- using var transformed = Transform3D(wall);
-
- using var outputStream = new SKFileWStream(outputPath);
- using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels());
- pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90);
- }
-
- /// <summary>
- /// Generates a collage of posters and landscape pictures.
- /// </summary>
- /// <param name="posters">The poster paths.</param>
- /// <param name="backdrops">The landscape paths.</param>
- /// <returns>The created collage as a bitmap.</returns>
- private SKBitmap GenerateCollage(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
- {
- var posterIndex = 0;
- var backdropIndex = 0;
-
- var bitmap = new SKBitmap(WallWidth, WallHeight);
- using var canvas = new SKCanvas(bitmap);
- canvas.Clear(SKColors.Black);
-
- int posterHeight = WallHeight / 6;
-
- for (int i = 0; i < Rows; i++)
- {
- int imageCounter = Random.Shared.Next(0, 5);
- int currentWidthPos = i * 75;
- int currentHeight = i * (posterHeight + Spacing);
-
- while (currentWidthPos < WallWidth)
- {
- SKBitmap? currentImage;
-
- switch (imageCounter)
- {
- case 0:
- case 2:
- case 3:
- currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
- posterIndex = newPosterIndex;
- break;
- default:
- currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
- backdropIndex = newBackdropIndex;
- break;
- }
-
- if (currentImage is null)
- {
- throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!");
- }
-
- // resize to the same aspect as the original
- var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
- using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
- currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
-
- // draw on canvas
- canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
-
- currentWidthPos += imageWidth + Spacing;
-
- currentImage.Dispose();
-
- if (imageCounter >= 4)
- {
- imageCounter = 0;
- }
- else
- {
- imageCounter++;
- }
- }
- }
-
- return bitmap;
- }
-
- /// <summary>
- /// Transform the collage in 3D space.
- /// </summary>
- /// <param name="input">The bitmap to transform.</param>
- /// <returns>The transformed image.</returns>
- private SKBitmap Transform3D(SKBitmap input)
- {
- var bitmap = new SKBitmap(FinalWidth, FinalHeight);
- using var canvas = new SKCanvas(bitmap);
- canvas.Clear(SKColors.Black);
- var matrix = new SKMatrix
- {
- ScaleX = 0.324108899f,
- ScaleY = 0.563934922f,
- SkewX = -0.244337708f,
- SkewY = 0.0377609022f,
- TransX = 42.0407715f,
- TransY = -198.104706f,
- Persp0 = -9.08959337E-05f,
- Persp1 = 6.85242048E-05f,
- Persp2 = 0.988209724f
- };
-
- canvas.SetMatrix(matrix);
- canvas.DrawBitmap(input, 0, 0);
-
- return bitmap;
- }
- }
-}
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
deleted file mode 100644
index c8b8f3ace..000000000
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ /dev/null
@@ -1,186 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text.RegularExpressions;
-using SkiaSharp;
-
-namespace Jellyfin.Drawing.Skia
-{
- /// <summary>
- /// Used to build collages of multiple images arranged in vertical strips.
- /// </summary>
- public class StripCollageBuilder
- {
- private readonly SkiaEncoder _skiaEncoder;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StripCollageBuilder"/> class.
- /// </summary>
- /// <param name="skiaEncoder">The encoder to use for building collages.</param>
- public StripCollageBuilder(SkiaEncoder skiaEncoder)
- {
- _skiaEncoder = skiaEncoder;
- }
-
- /// <summary>
- /// Check which format an image has been encoded with using its filename extension.
- /// </summary>
- /// <param name="outputPath">The path to the image to get the format for.</param>
- /// <returns>The image format.</returns>
- public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
- {
- ArgumentNullException.ThrowIfNull(outputPath);
-
- var ext = Path.GetExtension(outputPath);
-
- if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
- || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
- {
- return SKEncodedImageFormat.Jpeg;
- }
-
- if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
- {
- return SKEncodedImageFormat.Webp;
- }
-
- if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
- {
- return SKEncodedImageFormat.Gif;
- }
-
- if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
- {
- return SKEncodedImageFormat.Bmp;
- }
-
- // default to png
- return SKEncodedImageFormat.Png;
- }
-
- /// <summary>
- /// Create a square collage.
- /// </summary>
- /// <param name="paths">The paths of the images to use in the collage.</param>
- /// <param name="outputPath">The path at which to place the resulting collage image.</param>
- /// <param name="width">The desired width of the collage.</param>
- /// <param name="height">The desired height of the collage.</param>
- public void BuildSquareCollage(IReadOnlyList<string> paths, string outputPath, int width, int height)
- {
- using var bitmap = BuildSquareCollageBitmap(paths, width, height);
- using var outputStream = new SKFileWStream(outputPath);
- using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
- pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
- }
-
- /// <summary>
- /// Create a thumb collage.
- /// </summary>
- /// <param name="paths">The paths of the images to use in the collage.</param>
- /// <param name="outputPath">The path at which to place the resulting image.</param>
- /// <param name="width">The desired width of the collage.</param>
- /// <param name="height">The desired height of the collage.</param>
- /// <param name="libraryName">The name of the library to draw on the collage.</param>
- public void BuildThumbCollage(IReadOnlyList<string> paths, string outputPath, int width, int height, string? libraryName)
- {
- using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
- using var outputStream = new SKFileWStream(outputPath);
- using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
- pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
- }
-
- private SKBitmap BuildThumbCollageBitmap(IReadOnlyList<string> paths, int width, int height, string? libraryName)
- {
- var bitmap = new SKBitmap(width, height);
-
- using var canvas = new SKCanvas(bitmap);
- canvas.Clear(SKColors.Black);
-
- using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _);
- if (backdrop is null)
- {
- return bitmap;
- }
-
- // resize to the same aspect as the original
- var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
- using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
- // draw the backdrop
- canvas.DrawImage(residedBackdrop, 0, 0);
-
- // draw shadow rectangle
- using var paintColor = new SKPaint
- {
- Color = SKColors.Black.WithAlpha(0x78),
- Style = SKPaintStyle.Fill
- };
- canvas.DrawRect(0, 0, width, height, paintColor);
-
- var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
-
- // use the system fallback to find a typeface for the given CJK character
- var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
- var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
- if (!string.IsNullOrEmpty(filteredName))
- {
- typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
- }
-
- // draw library name
- using var textPaint = new SKPaint
- {
- Color = SKColors.White,
- Style = SKPaintStyle.Fill,
- TextSize = 112,
- TextAlign = SKTextAlign.Center,
- Typeface = typeFace,
- IsAntialias = true
- };
-
- // scale down text to 90% of the width if text is larger than 95% of the width
- var textWidth = textPaint.MeasureText(libraryName);
- if (textWidth > width * 0.95)
- {
- textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
- }
-
- canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
-
- return bitmap;
- }
-
- private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height)
- {
- var bitmap = new SKBitmap(width, height);
- var imageIndex = 0;
- var cellWidth = width / 2;
- var cellHeight = height / 2;
-
- using var canvas = new SKCanvas(bitmap);
- for (var x = 0; x < 2; x++)
- {
- for (var y = 0; y < 2; y++)
- {
- using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex);
- imageIndex = newIndex;
-
- if (currentBitmap is null)
- {
- continue;
- }
-
- // Scale image. The FromBitmap creates a copy
- var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
- using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
-
- // draw this image into the strip at the next position
- var xPos = x * cellWidth;
- var yPos = y * cellHeight;
- canvas.DrawBitmap(resizedBitmap, xPos, yPos);
- }
- }
-
- return bitmap;
- }
- }
-}
diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
deleted file mode 100644
index 58f887c96..000000000
--- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System.Globalization;
-using MediaBrowser.Model.Drawing;
-using SkiaSharp;
-
-namespace Jellyfin.Drawing.Skia
-{
- /// <summary>
- /// Static helper class for drawing unplayed count indicators.
- /// </summary>
- public static class UnplayedCountIndicator
- {
- /// <summary>
- /// The x-offset used when drawing an unplayed count indicator.
- /// </summary>
- private const int OffsetFromTopRightCorner = 38;
-
- /// <summary>
- /// Draw an unplayed count indicator in the top right corner of a canvas.
- /// </summary>
- /// <param name="canvas">The canvas to draw the indicator on.</param>
- /// <param name="imageSize">
- /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
- /// indicator.
- /// </param>
- /// <param name="count">The number to draw in the indicator.</param>
- public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count)
- {
- var x = imageSize.Width - OffsetFromTopRightCorner;
- var text = count.ToString(CultureInfo.InvariantCulture);
-
- using var paint = new SKPaint
- {
- Color = SKColor.Parse("#CC00A4DC"),
- Style = SKPaintStyle.Fill
- };
-
- canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
-
- paint.Color = new SKColor(255, 255, 255, 255);
- paint.TextSize = 24;
- paint.IsAntialias = true;
-
- var y = OffsetFromTopRightCorner + 9;
-
- if (text.Length == 1)
- {
- x -= 7;
- }
-
- if (text.Length == 2)
- {
- x -= 13;
- }
- else if (text.Length >= 3)
- {
- x -= 15;
- y -= 2;
- paint.TextSize = 18;
- }
-
- canvas.DrawText(text, x, y, paint);
- }
- }
-}
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj
index 975d1c8ce..2c153d88b 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -11,7 +11,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index 9d6ca6aab..ce1c54cbb 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity
/// </summary>
public class ActivityManager : IActivityManager
{
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
/// </summary>
/// <param name="provider">The Jellyfin database provider.</param>
- public ActivityManager(IDbContextFactory<JellyfinDb> provider)
+ public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
{
_provider = provider;
}
@@ -48,18 +48,10 @@ namespace Jellyfin.Server.Implementations.Activity
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- IQueryable<ActivityLog> entries = dbContext.ActivityLogs
- .OrderByDescending(entry => entry.DateCreated);
-
- if (query.MinDate.HasValue)
- {
- entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
- }
-
- if (query.HasUserId.HasValue)
- {
- entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
- }
+ var entries = dbContext.ActivityLogs
+ .OrderByDescending(entry => entry.DateCreated)
+ .Where(entry => query.MinDate == null || entry.DateCreated >= query.MinDate)
+ .Where(entry => !query.HasUserId.HasValue || entry.UserId.Equals(default) != query.HasUserId.Value);
return new QueryResult<ActivityLogEntry>(
query.Skip,
@@ -67,8 +59,16 @@ namespace Jellyfin.Server.Implementations.Activity
await entries
.Skip(query.Skip ?? 0)
.Take(query.Limit ?? 100)
- .AsAsyncEnumerable()
- .Select(ConvertToOldModel)
+ .Select(entity => new ActivityLogEntry(entity.Name, entity.Type, entity.UserId)
+ {
+ Id = entity.Id,
+ Overview = entity.Overview,
+ ShortOverview = entity.ShortOverview,
+ ItemId = entity.ItemId,
+ Date = entity.DateCreated,
+ Severity = entity.LogSeverity
+ })
+ .AsQueryable()
.ToListAsync()
.ConfigureAwait(false));
}
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index 15ac5c668..8b15d6823 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Devices
/// </summary>
public class DeviceManager : IDeviceManager
{
- private readonly IDbContextFactory<JellyfinDb> _dbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IUserManager _userManager;
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
@@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Devices
/// </summary>
/// <param name="dbProvider">The database provider.</param>
/// <param name="userManager">The user manager.</param>
- public DeviceManager(IDbContextFactory<JellyfinDb> dbProvider, IUserManager userManager)
+ public DeviceManager(IDbContextFactory<JellyfinDbContext> dbProvider, IUserManager userManager)
{
_dbProvider = dbProvider;
_userManager = userManager;
@@ -54,7 +54,7 @@ namespace Jellyfin.Server.Implementations.Devices
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
+ deviceOptions = await dbContext.DeviceOptions.FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
if (deviceOptions is null)
{
deviceOptions = new DeviceOptions(deviceId);
@@ -132,22 +132,11 @@ namespace Jellyfin.Server.Implementations.Devices
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- var devices = dbContext.Devices.AsQueryable();
-
- if (query.UserId.HasValue)
- {
- devices = devices.Where(device => device.UserId.Equals(query.UserId.Value));
- }
-
- if (query.DeviceId is not null)
- {
- devices = devices.Where(device => device.DeviceId == query.DeviceId);
- }
-
- if (query.AccessToken is not null)
- {
- devices = devices.Where(device => device.AccessToken == query.AccessToken);
- }
+ var devices = dbContext.Devices
+ .OrderBy(d => d.Id)
+ .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
+ .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
+ .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
var count = await devices.CountAsync().ConfigureAwait(false);
@@ -179,11 +168,10 @@ namespace Jellyfin.Server.Implementations.Devices
/// <inheritdoc />
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
{
- IAsyncEnumerable<Device> sessions;
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- sessions = dbContext.Devices
+ IAsyncEnumerable<Device> sessions = dbContext.Devices
.Include(d => d.User)
.OrderByDescending(d => d.DateLastActivity)
.ThenBy(d => d.DeviceId)
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index 05c622931..bb8d4dd14 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -4,7 +4,6 @@ using EFCoreSecondLevelCacheInterceptor;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.Extensions;
@@ -29,13 +28,11 @@ public static class ServiceCollectionExtensions
.SkipCachingResults(result =>
result.Value is null || (result.Value is EFTableRows rows && rows.RowsCount == 0)));
- serviceCollection.AddPooledDbContextFactory<JellyfinDb>((serviceProvider, opt) =>
+ serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
- var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
- .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>())
- .UseLoggerFactory(loggerFactory);
+ .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
});
return serviceCollection;
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index e98290673..b078db016 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -6,13 +6,9 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
- </PropertyGroup>
-
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@@ -26,15 +22,15 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.1" />
+ <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.2" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
deleted file mode 100644
index dc4f53913..000000000
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ /dev/null
@@ -1,162 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Entities.Security;
-using Jellyfin.Data.Interfaces;
-using Microsoft.EntityFrameworkCore;
-
-namespace Jellyfin.Server.Implementations
-{
- /// <inheritdoc/>
- public class JellyfinDb : DbContext
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="JellyfinDb"/> class.
- /// </summary>
- /// <param name="options">The database context options.</param>
- public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options)
- {
- }
-
- /// <summary>
- /// Gets or sets the default connection string.
- /// </summary>
- public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db";
-
- public virtual DbSet<AccessSchedule> AccessSchedules { get; set; }
-
- public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
-
- public virtual DbSet<ApiKey> ApiKeys { get; set; }
-
- public virtual DbSet<Device> Devices { get; set; }
-
- public virtual DbSet<DeviceOptions> DeviceOptions { get; set; }
-
- public virtual DbSet<DisplayPreferences> DisplayPreferences { get; set; }
-
- public virtual DbSet<ImageInfo> ImageInfos { get; set; }
-
- public virtual DbSet<ItemDisplayPreferences> ItemDisplayPreferences { get; set; }
-
- public virtual DbSet<CustomItemDisplayPreferences> CustomItemDisplayPreferences { get; set; }
-
- public virtual DbSet<Permission> Permissions { get; set; }
-
- public virtual DbSet<Preference> Preferences { get; set; }
-
- public virtual DbSet<User> Users { get; set; }
-
- /*public virtual DbSet<Artwork> Artwork { get; set; }
-
- public virtual DbSet<Book> Books { get; set; }
-
- public virtual DbSet<BookMetadata> BookMetadata { get; set; }
-
- public virtual DbSet<Chapter> Chapters { get; set; }
-
- public virtual DbSet<Collection> Collections { get; set; }
-
- public virtual DbSet<CollectionItem> CollectionItems { get; set; }
-
- public virtual DbSet<Company> Companies { get; set; }
-
- public virtual DbSet<CompanyMetadata> CompanyMetadata { get; set; }
-
- public virtual DbSet<CustomItem> CustomItems { get; set; }
-
- public virtual DbSet<CustomItemMetadata> CustomItemMetadata { get; set; }
-
- public virtual DbSet<Episode> Episodes { get; set; }
-
- public virtual DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
-
- public virtual DbSet<Genre> Genres { get; set; }
-
- public virtual DbSet<Group> Groups { get; set; }
-
- public virtual DbSet<Library> Libraries { get; set; }
-
- public virtual DbSet<LibraryItem> LibraryItems { get; set; }
-
- public virtual DbSet<LibraryRoot> LibraryRoot { get; set; }
-
- public virtual DbSet<MediaFile> MediaFiles { get; set; }
-
- public virtual DbSet<MediaFileStream> MediaFileStream { get; set; }
-
- public virtual DbSet<Metadata> Metadata { get; set; }
-
- public virtual DbSet<MetadataProvider> MetadataProviders { get; set; }
-
- public virtual DbSet<MetadataProviderId> MetadataProviderIds { get; set; }
-
- public virtual DbSet<Movie> Movies { get; set; }
-
- public virtual DbSet<MovieMetadata> MovieMetadata { get; set; }
-
- public virtual DbSet<MusicAlbum> MusicAlbums { get; set; }
-
- public virtual DbSet<MusicAlbumMetadata> MusicAlbumMetadata { get; set; }
-
- public virtual DbSet<Person> People { get; set; }
-
- public virtual DbSet<PersonRole> PersonRoles { get; set; }
-
- public virtual DbSet<Photo> Photo { get; set; }
-
- public virtual DbSet<PhotoMetadata> PhotoMetadata { get; set; }
-
- public virtual DbSet<ProviderMapping> ProviderMappings { get; set; }
-
- public virtual DbSet<Rating> Ratings { get; set; }
-
- /// <summary>
- /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
- /// store review ratings, not age ratings.
- /// </summary>
- public virtual DbSet<RatingSource> RatingSources { get; set; }
-
- public virtual DbSet<Release> Releases { get; set; }
-
- public virtual DbSet<Season> Seasons { get; set; }
-
- public virtual DbSet<SeasonMetadata> SeasonMetadata { get; set; }
-
- public virtual DbSet<Series> Series { get; set; }
-
- public virtual DbSet<SeriesMetadata> SeriesMetadata { get; set; }
-
- public virtual DbSet<Track> Tracks { get; set; }
-
- public virtual DbSet<TrackMetadata> TrackMetadata { get; set; }*/
-
- /// <inheritdoc/>
- public override int SaveChanges()
- {
- foreach (var saveEntity in ChangeTracker.Entries()
- .Where(e => e.State == EntityState.Modified)
- .Select(entry => entry.Entity)
- .OfType<IHasConcurrencyToken>())
- {
- saveEntity.OnSavingChanges();
- }
-
- return base.SaveChanges();
- }
-
- /// <inheritdoc />
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
- base.OnModelCreating(modelBuilder);
- modelBuilder.HasDefaultSchema("jellyfin");
-
- // Configuration for each entity is in it's own class inside 'ModelConfiguration'.
- modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDb).Assembly);
- }
- }
-}
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
new file mode 100644
index 000000000..0d91707e3
--- /dev/null
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Entities.Security;
+using Jellyfin.Data.Interfaces;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations;
+
+/// <inheritdoc/>
+public class JellyfinDbContext : DbContext
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
+ /// </summary>
+ /// <param name="options">The database context options.</param>
+ public JellyfinDbContext(DbContextOptions<JellyfinDbContext> options) : base(options)
+ {
+ }
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules.
+ /// </summary>
+ public DbSet<AccessSchedule> AccessSchedules => Set<AccessSchedule>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the activity logs.
+ /// </summary>
+ public DbSet<ActivityLog> ActivityLogs => Set<ActivityLog>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the API keys.
+ /// </summary>
+ public DbSet<ApiKey> ApiKeys => Set<ApiKey>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the devices.
+ /// </summary>
+ public DbSet<Device> Devices => Set<Device>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the device options.
+ /// </summary>
+ public DbSet<DeviceOptions> DeviceOptions => Set<DeviceOptions>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the display preferences.
+ /// </summary>
+ public DbSet<DisplayPreferences> DisplayPreferences => Set<DisplayPreferences>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the image infos.
+ /// </summary>
+ public DbSet<ImageInfo> ImageInfos => Set<ImageInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the item display preferences.
+ /// </summary>
+ public DbSet<ItemDisplayPreferences> ItemDisplayPreferences => Set<ItemDisplayPreferences>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the custom item display preferences.
+ /// </summary>
+ public DbSet<CustomItemDisplayPreferences> CustomItemDisplayPreferences => Set<CustomItemDisplayPreferences>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the permissions.
+ /// </summary>
+ public DbSet<Permission> Permissions => Set<Permission>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the preferences.
+ /// </summary>
+ public DbSet<Preference> Preferences => Set<Preference>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the users.
+ /// </summary>
+ public DbSet<User> Users => Set<User>();
+
+ /*public DbSet<Artwork> Artwork => Set<Artwork>();
+
+ public DbSet<Book> Books => Set<Book>();
+
+ public DbSet<BookMetadata> BookMetadata => Set<BookMetadata>();
+
+ public DbSet<Chapter> Chapters => Set<Chapter>();
+
+ public DbSet<Collection> Collections => Set<Collection>();
+
+ public DbSet<CollectionItem> CollectionItems => Set<CollectionItem>();
+
+ public DbSet<Company> Companies => Set<Company>();
+
+ public DbSet<CompanyMetadata> CompanyMetadata => Set<CompanyMetadata>();
+
+ public DbSet<CustomItem> CustomItems => Set<CustomItem>();
+
+ public DbSet<CustomItemMetadata> CustomItemMetadata => Set<CustomItemMetadata>();
+
+ public DbSet<Episode> Episodes => Set<Episode>();
+
+ public DbSet<EpisodeMetadata> EpisodeMetadata => Set<EpisodeMetadata>();
+
+ public DbSet<Genre> Genres => Set<Genre>();
+
+ public DbSet<Group> Groups => Set<Groups>();
+
+ public DbSet<Library> Libraries => Set<Library>();
+
+ public DbSet<LibraryItem> LibraryItems => Set<LibraryItems>();
+
+ public DbSet<LibraryRoot> LibraryRoot => Set<LibraryRoot>();
+
+ public DbSet<MediaFile> MediaFiles => Set<MediaFiles>();
+
+ public DbSet<MediaFileStream> MediaFileStream => Set<MediaFileStream>();
+
+ public DbSet<Metadata> Metadata => Set<Metadata>();
+
+ public DbSet<MetadataProvider> MetadataProviders => Set<MetadataProvider>();
+
+ public DbSet<MetadataProviderId> MetadataProviderIds => Set<MetadataProviderId>();
+
+ public DbSet<Movie> Movies => Set<Movie>();
+
+ public DbSet<MovieMetadata> MovieMetadata => Set<MovieMetadata>();
+
+ public DbSet<MusicAlbum> MusicAlbums => Set<MusicAlbum>();
+
+ public DbSet<MusicAlbumMetadata> MusicAlbumMetadata => Set<MusicAlbumMetadata>();
+
+ public DbSet<Person> People => Set<Person>();
+
+ public DbSet<PersonRole> PersonRoles => Set<PersonRole>();
+
+ public DbSet<Photo> Photo => Set<Photo>();
+
+ public DbSet<PhotoMetadata> PhotoMetadata => Set<PhotoMetadata>();
+
+ public DbSet<ProviderMapping> ProviderMappings => Set<ProviderMapping>();
+
+ public DbSet<Rating> Ratings => Set<Rating>();
+
+ /// <summary>
+ /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
+ /// store review ratings, not age ratings.
+ /// </summary>
+ public DbSet<RatingSource> RatingSources => Set<RatingSource>();
+
+ public DbSet<Release> Releases => Set<Release>();
+
+ public DbSet<Season> Seasons => Set<Season>();
+
+ public DbSet<SeasonMetadata> SeasonMetadata => Set<SeasonMetadata>();
+
+ public DbSet<Series> Series => Set<Series>();
+
+ public DbSet<SeriesMetadata> SeriesMetadata => Set<SeriesMetadata();
+
+ public DbSet<Track> Tracks => Set<Track>();
+
+ public DbSet<TrackMetadata> TrackMetadata => Set<TrackMetadata>();*/
+
+ /// <inheritdoc/>
+ public override int SaveChanges()
+ {
+ foreach (var saveEntity in ChangeTracker.Entries()
+ .Where(e => e.State == EntityState.Modified)
+ .Select(entry => entry.Entity)
+ .OfType<IHasConcurrencyToken>())
+ {
+ saveEntity.OnSavingChanges();
+ }
+
+ return base.SaveChanges();
+ }
+
+ /// <inheritdoc />
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
+ base.OnModelCreating(modelBuilder);
+
+ // Configuration for each entity is in it's own class inside 'ModelConfiguration'.
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
index 98a83b745..4be6c2faa 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20200514181226_AddActivityLog")]
partial class AddActivityLog
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs
index 6342ce9cf..f3254734a 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20200613202153_AddUsers")]
partial class AddUsers
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
index d44707d06..12d6faa8f 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20200728005145_AddDisplayPreferences")]
partial class AddDisplayPreferences
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs
index 2234f9d5f..f1cc20805 100644
--- a/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20200905220533_FixDisplayPreferencesIndex")]
partial class FixDisplayPreferencesIndex
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
index e5c326a32..f134d363c 100644
--- a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20201004171403_AddMaxActiveSessions")]
partial class AddMaxActiveSessions
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs
index 10663d065..ec65205d4 100644
--- a/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20201204223655_AddCustomDisplayPreferences")]
partial class AddCustomDisplayPreferences
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs
index 869676824..45dad6be6 100644
--- a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20210320181425_AddIndexesAndCollations")]
partial class AddIndexesAndCollations
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
index d332d19f2..eff84b457 100644
--- a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20210407110544_NullableCustomPrefValue")]
partial class NullableCustomPrefValue
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
index 7e9566e2e..ad7c2dd2c 100644
--- a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
@@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20210814002109_AddDevices")]
partial class AddDevices
{
diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
index 03e3f3c92..f9497a3b6 100644
--- a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
+++ b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
[Migration("20221022080052_AddIndexActivityLogsDateCreated")]
partial class AddIndexActivityLogsDateCreated
{
diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
index 72a4a8c3b..940cf7c5d 100644
--- a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
+++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
@@ -4,17 +4,17 @@ using Microsoft.EntityFrameworkCore.Design;
namespace Jellyfin.Server.Implementations.Migrations
{
/// <summary>
- /// The design time factory for <see cref="JellyfinDb"/>.
+ /// The design time factory for <see cref="JellyfinDbContext"/>.
/// This is only used for the creation of migrations and not during runtime.
/// </summary>
- internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory<JellyfinDb>
+ internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory<JellyfinDbContext>
{
- public JellyfinDb CreateDbContext(string[] args)
+ public JellyfinDbContext CreateDbContext(string[] args)
{
- var optionsBuilder = new DbContextOptionsBuilder<JellyfinDb>();
+ var optionsBuilder = new DbContextOptionsBuilder<JellyfinDbContext>();
optionsBuilder.UseSqlite("Data Source=jellyfin.db");
- return new JellyfinDb(optionsBuilder.Options);
+ return new JellyfinDbContext(optionsBuilder.Options);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 2dd7b094a..dd5f7f012 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
- [DbContext(typeof(JellyfinDb))]
+ [DbContext(typeof(JellyfinDbContext))]
partial class JellyfinDbModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
index 810e57807..b2dfe60a1 100644
--- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
@@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security
/// <inheritdoc />
public class AuthenticationManager : IAuthenticationManager
{
- private readonly IDbContextFactory<JellyfinDb> _dbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationManager"/> class.
/// </summary>
/// <param name="dbProvider">The database provider.</param>
- public AuthenticationManager(IDbContextFactory<JellyfinDb> dbProvider)
+ public AuthenticationManager(IDbContextFactory<JellyfinDbContext> dbProvider)
{
_dbProvider = dbProvider;
}
@@ -40,7 +40,6 @@ namespace Jellyfin.Server.Implementations.Security
await using (dbContext.ConfigureAwait(false))
{
return await dbContext.ApiKeys
- .AsAsyncEnumerable()
.Select(key => new AuthenticationInfo
{
AppName = key.Name,
@@ -60,7 +59,6 @@ namespace Jellyfin.Server.Implementations.Security
await using (dbContext.ConfigureAwait(false))
{
var key = await dbContext.ApiKeys
- .AsQueryable()
.Where(apiKey => apiKey.AccessToken == accessToken)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index ec5742bab..63d3e8a04 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -16,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security
{
public class AuthorizationContext : IAuthorizationContext
{
- private readonly IDbContextFactory<JellyfinDb> _jellyfinDbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
private readonly IUserManager _userManager;
private readonly IServerApplicationHost _serverApplicationHost;
public AuthorizationContext(
- IDbContextFactory<JellyfinDb> jellyfinDb,
+ IDbContextFactory<JellyfinDbContext> jellyfinDb,
IUserManager userManager,
IServerApplicationHost serverApplicationHost)
{
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index 4fda8f5a4..960195467 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -54,7 +54,8 @@ namespace Jellyfin.Server.Implementations.Users
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
SerializablePasswordReset spr;
- await using (var str = AsyncFile.OpenRead(resetFile))
+ var str = AsyncFile.OpenRead(resetFile);
+ await using (str.ConfigureAwait(false))
{
spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false)
?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid.");
@@ -107,7 +108,8 @@ namespace Jellyfin.Server.Implementations.Users
UserName = user.Username
};
- await using (FileStream fileStream = AsyncFile.OpenWrite(filePath))
+ FileStream fileStream = AsyncFile.OpenWrite(filePath);
+ await using (fileStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index fddad1c4f..bfae81e4c 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Users
/// </summary>
public class DisplayPreferencesManager : IDisplayPreferencesManager
{
- private readonly JellyfinDb _dbContext;
+ private readonly JellyfinDbContext _dbContext;
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
/// </summary>
/// <param name="dbContextFactory">The database context factory.</param>
- public DisplayPreferencesManager(IDbContextFactory<JellyfinDb> dbContextFactory)
+ public DisplayPreferencesManager(IDbContextFactory<JellyfinDbContext> dbContextFactory)
{
_dbContext = dbContextFactory.CreateDbContext();
}
@@ -62,7 +62,6 @@ namespace Jellyfin.Server.Implementations.Users
public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
{
return _dbContext.ItemDisplayPreferences
- .AsQueryable()
.Where(prefs => prefs.UserId.Equals(userId) && !prefs.ItemId.Equals(default) && string.Equals(prefs.Client, client))
.ToList();
}
@@ -71,7 +70,6 @@ namespace Jellyfin.Server.Implementations.Users
public Dictionary<string, string?> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
return _dbContext.CustomItemDisplayPreferences
- .AsQueryable()
.Where(prefs => prefs.UserId.Equals(userId)
&& prefs.ItemId.Equals(itemId)
&& string.Equals(prefs.Client, client))
@@ -82,7 +80,6 @@ namespace Jellyfin.Server.Implementations.Users
public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences)
{
var existingPrefs = _dbContext.CustomItemDisplayPreferences
- .AsQueryable()
.Where(prefs => prefs.UserId.Equals(userId)
&& prefs.ItemId.Equals(itemId)
&& string.Equals(prefs.Client, client));
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index ae3fcad29..dc9d78857 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users
/// </summary>
public class UserManager : IUserManager
{
- private readonly IDbContextFactory<JellyfinDb> _dbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IEventManager _eventManager;
private readonly ICryptoProvider _cryptoProvider;
private readonly INetworkManager _networkManager;
@@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
public UserManager(
- IDbContextFactory<JellyfinDb> dbProvider,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager,
ICryptoProvider cryptoProvider,
INetworkManager networkManager,
@@ -85,6 +85,7 @@ namespace Jellyfin.Server.Implementations.Users
_users = new ConcurrentDictionary<Guid, User>();
using var dbContext = _dbProvider.CreateDbContext();
foreach (var user in dbContext.Users
+ .AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
@@ -143,7 +144,6 @@ namespace Jellyfin.Server.Implementations.Users
await using (dbContext.ConfigureAwait(false))
{
if (await dbContext.Users
- .AsQueryable()
.AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
.ConfigureAwait(false))
{
@@ -157,7 +157,9 @@ namespace Jellyfin.Server.Implementations.Users
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
}
- OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
+ var eventArgs = new UserUpdatedEventArgs(user);
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+ OnUserUpdated?.Invoke(this, eventArgs);
}
/// <inheritdoc/>
@@ -170,7 +172,7 @@ namespace Jellyfin.Server.Implementations.Users
}
}
- internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
+ internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext dbContext)
{
// TODO: Remove after user item data is migrated.
var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
@@ -884,7 +886,7 @@ namespace Jellyfin.Server.Implementations.Users
await UpdateUserAsync(user).ConfigureAwait(false);
}
- private async Task UpdateUserInternalAsync(JellyfinDb dbContext, User user)
+ private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{
dbContext.Users.Update(user);
_users[user.Id] = user;
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 002193baf..40cd5a044 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Reflection;
-using Emby.Drawing;
using Emby.Server.Implementations;
using Emby.Server.Implementations.Session;
using Jellyfin.Api.WebSocketListeners;
+using Jellyfin.Drawing;
using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
@@ -107,7 +107,7 @@ namespace Jellyfin.Server
yield return typeof(CoreAppHost).Assembly;
// Jellyfin.Server.Implementations
- yield return typeof(JellyfinDb).Assembly;
+ yield return typeof(JellyfinDbContext).Assembly;
}
/// <inheritdoc />
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index e29167747..463ca7321 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
+using Jellyfin.Api.Middleware;
using Jellyfin.Networking.Configuration;
-using Jellyfin.Server.Middleware;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.OpenApi.Models;
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index e8a51c2aa..5065fbdbb 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -20,13 +20,13 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Auth.SyncPlayAccessPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
+using Jellyfin.Api.Formatters;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
-using Jellyfin.Server.Formatters;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
new file mode 100644
index 000000000..58d3e1b2d
--- /dev/null
+++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
@@ -0,0 +1,90 @@
+using System;
+using System.IO;
+using System.Net;
+using Jellyfin.Server.Helpers;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Extensions;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Extensions;
+
+/// <summary>
+/// Extensions for configuring the web host builder.
+/// </summary>
+public static class WebHostBuilderExtensions
+{
+ /// <summary>
+ /// Configure the web host builder.
+ /// </summary>
+ /// <param name="builder">The builder to configure.</param>
+ /// <param name="appHost">The application host.</param>
+ /// <param name="startupConfig">The application configuration.</param>
+ /// <param name="appPaths">The application paths.</param>
+ /// <param name="logger">The logger.</param>
+ /// <returns>The configured web host builder.</returns>
+ public static IWebHostBuilder ConfigureWebHostBuilder(
+ this IWebHostBuilder builder,
+ CoreAppHost appHost,
+ IConfiguration startupConfig,
+ IApplicationPaths appPaths,
+ ILogger logger)
+ {
+ return builder
+ .UseKestrel((builderContext, options) =>
+ {
+ var addresses = appHost.NetManager.GetAllBindInterfaces();
+
+ bool flagged = false;
+ foreach (IPObject netAdd in addresses)
+ {
+ logger.LogInformation("Kestrel listening on {Address}", IPAddress.IPv6Any.Equals(netAdd.Address) ? "All Addresses" : netAdd);
+ options.Listen(netAdd.Address, appHost.HttpPort);
+ if (appHost.ListenWithHttps)
+ {
+ options.Listen(
+ netAdd.Address,
+ appHost.HttpsPort,
+ listenOptions => listenOptions.UseHttps(appHost.Certificate));
+ }
+ else if (builderContext.HostingEnvironment.IsDevelopment())
+ {
+ try
+ {
+ options.Listen(
+ netAdd.Address,
+ appHost.HttpsPort,
+ listenOptions => listenOptions.UseHttps());
+ }
+ catch (InvalidOperationException)
+ {
+ if (!flagged)
+ {
+ logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted");
+ flagged = true;
+ }
+ }
+ }
+ }
+
+ // Bind to unix socket (only on unix systems)
+ if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix)
+ {
+ var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths);
+
+ // Workaround for https://github.com/aspnet/AspNetCore/issues/14134
+ if (File.Exists(socketPath))
+ {
+ File.Delete(socketPath);
+ }
+
+ options.ListenUnixSocket(socketPath);
+ logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
+ }
+ })
+ .UseStartup(_ => new Startup(appHost));
+ }
+}
diff --git a/Jellyfin.Server/Filters/FileRequestFilter.cs b/Jellyfin.Server/Filters/FileRequestFilter.cs
index 69e10994f..bb5d6a412 100644
--- a/Jellyfin.Server/Filters/FileRequestFilter.cs
+++ b/Jellyfin.Server/Filters/FileRequestFilter.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters
{
if (attribute is AcceptsFileAttribute acceptsFileAttribute)
{
- operation.RequestBody = GetRequestBody(acceptsFileAttribute.GetContentTypes());
+ operation.RequestBody = GetRequestBody(acceptsFileAttribute.ContentTypes);
break;
}
}
diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs
index 544fdbfd6..1a4559d26 100644
--- a/Jellyfin.Server/Filters/FileResponseFilter.cs
+++ b/Jellyfin.Server/Filters/FileResponseFilter.cs
@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Filters
response.Value.Content.Clear();
// Add all content-types as file.
- foreach (var contentType in producesFileAttribute.GetContentTypes())
+ foreach (var contentType in producesFileAttribute.ContentTypes)
{
response.Value.Content.Add(contentType, _openApiMediaType);
}
diff --git a/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs b/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs
new file mode 100644
index 000000000..bf00dcd53
--- /dev/null
+++ b/Jellyfin.Server/HealthChecks/DbContextFactoryHealthCheck.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Jellyfin.Server.HealthChecks;
+
+/// <summary>
+/// Implementation of the <see cref="DbContextHealthCheck{TContext}"/> for a <see cref="IDbContextFactory{TContext}"/>.
+/// </summary>
+/// <typeparam name="TContext">The type of database context.</typeparam>
+public class DbContextFactoryHealthCheck<TContext> : IHealthCheck
+ where TContext : DbContext
+{
+ private readonly IDbContextFactory<TContext> _dbContextFactory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DbContextFactoryHealthCheck{TContext}"/> class.
+ /// </summary>
+ /// <param name="contextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param>
+ public DbContextFactoryHealthCheck(IDbContextFactory<TContext> contextFactory)
+ {
+ _dbContextFactory = contextFactory;
+ }
+
+ /// <inheritdoc />
+ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ if (await dbContext.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false))
+ {
+ return HealthCheckResult.Healthy();
+ }
+ }
+
+ return HealthCheckResult.Unhealthy();
+ }
+}
diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs
new file mode 100644
index 000000000..f1bb9b283
--- /dev/null
+++ b/Jellyfin.Server/Helpers/StartupHelpers.cs
@@ -0,0 +1,326 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Runtime.Versioning;
+using System.Text;
+using System.Threading.Tasks;
+using Emby.Server.Implementations;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using SQLitePCL;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace Jellyfin.Server.Helpers;
+
+/// <summary>
+/// A class containing helper methods for server startup.
+/// </summary>
+public static class StartupHelpers
+{
+ /// <summary>
+ /// Create the data, config and log paths from the variety of inputs(command line args,
+ /// environment variables) or decide on what default to use. For Windows it's %AppPath%
+ /// for everything else the
+ /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a>
+ /// is followed.
+ /// </summary>
+ /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param>
+ /// <returns><see cref="ServerApplicationPaths" />.</returns>
+ public static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
+ {
+ // dataDir
+ // IF --datadir
+ // ELSE IF $JELLYFIN_DATA_DIR
+ // ELSE IF windows, use <%APPDATA%>/jellyfin
+ // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin
+ // ELSE use $HOME/.local/share/jellyfin
+ var dataDir = options.DataDir;
+ if (string.IsNullOrEmpty(dataDir))
+ {
+ dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR");
+
+ if (string.IsNullOrEmpty(dataDir))
+ {
+ // LocalApplicationData follows the XDG spec on unix machines
+ dataDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "jellyfin");
+ }
+ }
+
+ // configDir
+ // IF --configdir
+ // ELSE IF $JELLYFIN_CONFIG_DIR
+ // ELSE IF --datadir, use <datadir>/config (assume portable run)
+ // ELSE IF <datadir>/config exists, use that
+ // ELSE IF windows, use <datadir>/config
+ // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin
+ // ELSE $HOME/.config/jellyfin
+ var configDir = options.ConfigDir;
+ if (string.IsNullOrEmpty(configDir))
+ {
+ configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
+
+ if (string.IsNullOrEmpty(configDir))
+ {
+ if (options.DataDir is not null
+ || Directory.Exists(Path.Combine(dataDir, "config"))
+ || OperatingSystem.IsWindows())
+ {
+ // Hang config folder off already set dataDir
+ configDir = Path.Combine(dataDir, "config");
+ }
+ else
+ {
+ // $XDG_CONFIG_HOME defines the base directory relative to which
+ // user specific configuration files should be stored.
+ configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
+
+ // If $XDG_CONFIG_HOME is either not set or empty,
+ // a default equal to $HOME /.config should be used.
+ if (string.IsNullOrEmpty(configDir))
+ {
+ configDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ ".config");
+ }
+
+ configDir = Path.Combine(configDir, "jellyfin");
+ }
+ }
+ }
+
+ // cacheDir
+ // IF --cachedir
+ // ELSE IF $JELLYFIN_CACHE_DIR
+ // ELSE IF windows, use <datadir>/cache
+ // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin
+ // ELSE HOME/.cache/jellyfin
+ var cacheDir = options.CacheDir;
+ if (string.IsNullOrEmpty(cacheDir))
+ {
+ cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
+
+ if (string.IsNullOrEmpty(cacheDir))
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ // Hang cache folder off already set dataDir
+ cacheDir = Path.Combine(dataDir, "cache");
+ }
+ else
+ {
+ // $XDG_CACHE_HOME defines the base directory relative to which
+ // user specific non-essential data files should be stored.
+ cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
+
+ // If $XDG_CACHE_HOME is either not set or empty,
+ // a default equal to $HOME/.cache should be used.
+ if (string.IsNullOrEmpty(cacheDir))
+ {
+ cacheDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ ".cache");
+ }
+
+ cacheDir = Path.Combine(cacheDir, "jellyfin");
+ }
+ }
+ }
+
+ // webDir
+ // IF --webdir
+ // ELSE IF $JELLYFIN_WEB_DIR
+ // ELSE <bindir>/jellyfin-web
+ var webDir = options.WebDir;
+ if (string.IsNullOrEmpty(webDir))
+ {
+ webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
+
+ if (string.IsNullOrEmpty(webDir))
+ {
+ // Use default location under ResourcesPath
+ webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web");
+ }
+ }
+
+ // logDir
+ // IF --logdir
+ // ELSE IF $JELLYFIN_LOG_DIR
+ // ELSE IF --datadir, use <datadir>/log (assume portable run)
+ // ELSE <datadir>/log
+ var logDir = options.LogDir;
+ if (string.IsNullOrEmpty(logDir))
+ {
+ logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
+
+ if (string.IsNullOrEmpty(logDir))
+ {
+ // Hang log folder off already set dataDir
+ logDir = Path.Combine(dataDir, "log");
+ }
+ }
+
+ // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
+ dataDir = Path.GetFullPath(dataDir);
+ logDir = Path.GetFullPath(logDir);
+ configDir = Path.GetFullPath(configDir);
+ cacheDir = Path.GetFullPath(cacheDir);
+ webDir = Path.GetFullPath(webDir);
+
+ // Ensure the main folders exist before we continue
+ try
+ {
+ Directory.CreateDirectory(dataDir);
+ Directory.CreateDirectory(logDir);
+ Directory.CreateDirectory(configDir);
+ Directory.CreateDirectory(cacheDir);
+ }
+ catch (IOException ex)
+ {
+ Console.Error.WriteLine("Error whilst attempting to create folder");
+ Console.Error.WriteLine(ex.ToString());
+ Environment.Exit(1);
+ }
+
+ return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
+ }
+
+ /// <summary>
+ /// Gets the path for the unix socket Kestrel should bind to.
+ /// </summary>
+ /// <param name="startupConfig">The startup config.</param>
+ /// <param name="appPaths">The application paths.</param>
+ /// <returns>The path for Kestrel to bind to.</returns>
+ public static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths)
+ {
+ var socketPath = startupConfig.GetUnixSocketPath();
+
+ if (string.IsNullOrEmpty(socketPath))
+ {
+ var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
+ var socketFile = "jellyfin.sock";
+ if (xdgRuntimeDir is null)
+ {
+ // Fall back to config dir
+ socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, socketFile);
+ }
+ else
+ {
+ socketPath = Path.Join(xdgRuntimeDir, socketFile);
+ }
+ }
+
+ return socketPath;
+ }
+
+ /// <summary>
+ /// Sets the unix file permissions for Kestrel's socket file.
+ /// </summary>
+ /// <param name="startupConfig">The startup config.</param>
+ /// <param name="socketPath">The socket path.</param>
+ /// <param name="logger">The logger.</param>
+ [UnsupportedOSPlatform("windows")]
+ public static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath, ILogger logger)
+ {
+ var socketPerms = startupConfig.GetUnixSocketPermissions();
+
+ if (!string.IsNullOrEmpty(socketPerms))
+ {
+ File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8));
+ logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms);
+ }
+ }
+
+ /// <summary>
+ /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
+ /// already.
+ /// </summary>
+ /// <param name="appPaths">The application paths.</param>
+ /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns>
+ public static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
+ {
+ // Do nothing if the config file already exists
+ string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, Program.LoggingConfigFileDefault);
+ if (File.Exists(configPath))
+ {
+ return;
+ }
+
+ // Get a stream of the resource contents
+ // NOTE: The .csproj name is used instead of the assembly name in the resource path
+ const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
+ Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
+ ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
+ await using (resource.ConfigureAwait(false))
+ {
+ Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await using (dst.ConfigureAwait(false))
+ {
+ // Copy the resource contents to the expected file path for the config file
+ await resource.CopyToAsync(dst).ConfigureAwait(false);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Initialize Serilog using configuration and fall back to defaults on failure.
+ /// </summary>
+ /// <param name="configuration">The configuration object.</param>
+ /// <param name="appPaths">The application paths.</param>
+ public static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
+ {
+ try
+ {
+ // Serilog.Log is used by SerilogLoggerFactory when no logger is specified
+ Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(configuration)
+ .Enrich.FromLogContext()
+ .Enrich.WithThreadId()
+ .CreateLogger();
+ }
+ catch (Exception ex)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console(
+ outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}",
+ formatProvider: CultureInfo.InvariantCulture)
+ .WriteTo.Async(x => x.File(
+ Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
+ rollingInterval: RollingInterval.Day,
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
+ formatProvider: CultureInfo.InvariantCulture,
+ encoding: Encoding.UTF8))
+ .Enrich.FromLogContext()
+ .Enrich.WithThreadId()
+ .CreateLogger();
+
+ Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
+ }
+ }
+
+ /// <summary>
+ /// Call static initialization methods for the application.
+ /// </summary>
+ public static void PerformStaticInitialization()
+ {
+ // Make sure we have all the code pages we can get
+ // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+
+ // Increase the max http request limit
+ // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
+ ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
+
+ // Disable the "Expect: 100-Continue" header by default
+ // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
+ ServicePointManager.Expect100Continue = false;
+
+ Batteries_V2.Init();
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index ac2086935..9ea8508f2 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -24,7 +24,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@@ -37,24 +37,24 @@
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.1" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.2" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.2" />
<PackageReference Include="prometheus-net" Version="7.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="7.0.0" />
- <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
+ <PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" />
- <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.3" />
+ <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
+ <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
<ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
- <ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" />
+ <ProjectReference Include="..\src\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" />
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
</ItemGroup>
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index e9a45c140..23fb9e370 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -38,7 +38,6 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.ReaddDefaultPluginRepository),
typeof(Routines.MigrateDisplayPreferencesDb),
typeof(Routines.RemoveDownloadImagesInAdvance),
- typeof(Routines.AddPeopleQueryIndex),
typeof(Routines.MigrateAuthenticationDb)
};
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
index f6d8c9cc0..9e12c2e6b 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
@@ -38,7 +38,7 @@ namespace Jellyfin.Server.Migrations.Routines
/// <inheritdoc/>
public void Perform()
{
- _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo);
+ _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo };
_serverConfigurationManager.SaveConfiguration();
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs b/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs
deleted file mode 100644
index 6343c422d..000000000
--- a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System;
-using System.IO;
-using MediaBrowser.Controller;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Jellyfin.Server.Migrations.Routines
-{
- /// <summary>
- /// Migration to add table indexes to optimize the Persons query.
- /// </summary>
- public class AddPeopleQueryIndex : IMigrationRoutine
- {
- private const string DbFilename = "library.db";
- private readonly ILogger<AddPeopleQueryIndex> _logger;
- private readonly IServerApplicationPaths _serverApplicationPaths;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="AddPeopleQueryIndex"/> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{AddPeopleQueryIndex}"/> interface.</param>
- /// <param name="serverApplicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
- public AddPeopleQueryIndex(ILogger<AddPeopleQueryIndex> logger, IServerApplicationPaths serverApplicationPaths)
- {
- _logger = logger;
- _serverApplicationPaths = serverApplicationPaths;
- }
-
- /// <inheritdoc />
- public Guid Id => new Guid("DE009B59-BAAE-428D-A810-F67762DC05B8");
-
- /// <inheritdoc />
- public string Name => "AddPeopleQueryIndex";
-
- /// <inheritdoc />
- public bool PerformOnNewInstall => true;
-
- /// <inheritdoc />
- public void Perform()
- {
- var databasePath = Path.Join(_serverApplicationPaths.DataPath, DbFilename);
- using var connection = SQLite3.Open(databasePath, ConnectionFlags.ReadWrite, null);
- _logger.LogInformation("Creating index idx_TypedBaseItemsUserDataKeyType");
- connection.Execute("CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);");
- _logger.LogInformation("Creating index idx_PeopleNameListOrder");
- connection.Execute("CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder);");
- }
- }
-}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
index bf66f75ff..e8a0af9f8 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines
private const string DbFilename = "activitylog.db";
private readonly ILogger<MigrateActivityLogDb> _logger;
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
private readonly IServerApplicationPaths _paths;
/// <summary>
@@ -28,7 +28,7 @@ namespace Jellyfin.Server.Migrations.Routines
/// <param name="logger">The logger.</param>
/// <param name="paths">The server application paths.</param>
/// <param name="provider">The database provider.</param>
- public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDb> provider)
+ public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDbContext> provider)
{
_logger = logger;
_provider = provider;
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
index bf1ea8233..09daae0ff 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Server.Migrations.Routines
private const string DbFilename = "authentication.db";
private readonly ILogger<MigrateAuthenticationDb> _logger;
- private readonly IDbContextFactory<JellyfinDb> _dbProvider;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IServerApplicationPaths _appPaths;
private readonly IUserManager _userManager;
@@ -33,7 +33,7 @@ namespace Jellyfin.Server.Migrations.Routines
/// <param name="userManager">The user manager.</param>
public MigrateAuthenticationDb(
ILogger<MigrateAuthenticationDb> logger,
- IDbContextFactory<JellyfinDb> dbProvider,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
IServerApplicationPaths appPaths,
IUserManager userManager)
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 0fad77cfe..4b692d14f 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -25,7 +25,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
private readonly IServerApplicationPaths _paths;
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
private readonly JsonSerializerOptions _jsonOptions;
private readonly IUserManager _userManager;
@@ -39,7 +39,7 @@ namespace Jellyfin.Server.Migrations.Routines
public MigrateDisplayPreferencesDb(
ILogger<MigrateDisplayPreferencesDb> logger,
IServerApplicationPaths paths,
- IDbContextFactory<JellyfinDb> provider,
+ IDbContextFactory<JellyfinDbContext> provider,
IUserManager userManager)
{
_logger = logger;
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 2dbd82e8f..ea2f03302 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger<MigrateUserDb> _logger;
private readonly IServerApplicationPaths _paths;
- private readonly IDbContextFactory<JellyfinDb> _provider;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
private readonly IXmlSerializer _xmlSerializer;
/// <summary>
@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines
public MigrateUserDb(
ILogger<MigrateUserDb> logger,
IServerApplicationPaths paths,
- IDbContextFactory<JellyfinDb> provider,
+ IDbContextFactory<JellyfinDbContext> provider,
IXmlSerializer xmlSerializer)
{
_logger = logger;
diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
index 394f14d63..9cfaec46f 100644
--- a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
@@ -39,9 +39,9 @@ namespace Jellyfin.Server.Migrations.Routines
public void Perform()
{
// Only add if repository list is empty
- if (_serverConfigurationManager.Configuration.PluginRepositories.Count == 0)
+ if (_serverConfigurationManager.Configuration.PluginRepositories.Length == 0)
{
- _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo);
+ _serverConfigurationManager.Configuration.PluginRepositories = new[] { _defaultRepositoryInfo };
_serverConfigurationManager.SaveConfiguration();
}
}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 125a09478..25fe30a39 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -1,33 +1,26 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.IO;
using System.Linq;
-using System.Net;
using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
+using Jellyfin.Server.Extensions;
+using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.IO;
-using Microsoft.AspNetCore.Hosting;
+using MediaBrowser.Controller;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
-using SQLitePCL;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -48,8 +41,9 @@ namespace Jellyfin.Server
/// </summary>
public const string LoggingConfigFileSystem = "logging.json";
- private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
+ private static CancellationTokenSource _tokenSource = new();
+ private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
@@ -94,14 +88,14 @@ namespace Jellyfin.Server
private static async Task StartApp(StartupOptions options)
{
- var startTimestamp = Stopwatch.GetTimestamp();
+ _startTimestamp = Stopwatch.GetTimestamp();
// Log all uncaught exceptions to std error
static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
- Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject.ToString());
+ Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject);
AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
- ServerApplicationPaths appPaths = CreateApplicationPaths(options);
+ ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
@@ -110,13 +104,12 @@ namespace Jellyfin.Server
Environment.SetEnvironmentVariable("NEOReadDebugKeys", "1");
Environment.SetEnvironmentVariable("EnableExtendedVaFormats", "1");
- await InitLoggingConfigFile(appPaths).ConfigureAwait(false);
+ await StartupHelpers.InitLoggingConfigFile(appPaths).ConfigureAwait(false);
// Create an instance of the application configuration to use for application startup
IConfiguration startupConfig = CreateAppConfiguration(options, appPaths);
- // Initialize logging framework
- InitializeLoggingFramework(startupConfig, appPaths);
+ StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main");
// Log uncaught exceptions to the logging instead of std error
@@ -160,14 +153,14 @@ namespace Jellyfin.Server
// If hosting the web client, validate the client content path
if (startupConfig.HostWebClient())
{
- string? webContentPath = appPaths.WebPath;
+ var webContentPath = appPaths.WebPath;
if (!Directory.Exists(webContentPath) || !Directory.EnumerateFiles(webContentPath).Any())
{
_logger.LogError(
"The server is expected to host the web client, but the provided content directory is either " +
"invalid or empty: {WebContentPath}. If you do not want to host the web client with the " +
"server, you may set the '--nowebclient' command line flag, or set" +
- "'{ConfigKey}=false' in your config settings.",
+ "'{ConfigKey}=false' in your config settings",
webContentPath,
HostWebClientKey);
Environment.ExitCode = 1;
@@ -175,48 +168,66 @@ namespace Jellyfin.Server
}
}
- PerformStaticInitialization();
+ StartupHelpers.PerformStaticInitialization();
Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory);
+ do
+ {
+ _restartOnShutdown = false;
+ await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
+
+ if (_restartOnShutdown)
+ {
+ _tokenSource = new CancellationTokenSource();
+ _startTimestamp = Stopwatch.GetTimestamp();
+ }
+ } while (_restartOnShutdown);
+ }
+
+ private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
+ {
var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
startupConfig);
+ IHost? host = null;
try
{
- var serviceCollection = new ServiceCollection();
- appHost.Init(serviceCollection);
-
- var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
+ host = Host.CreateDefaultBuilder()
+ .ConfigureServices(services => appHost.Init(services))
+ .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
+ .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
+ .UseSerilog()
+ .Build();
- // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection.
- appHost.ServiceProvider = webHost.Services;
+ // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
+ appHost.ServiceProvider = host.Services;
await appHost.InitializeServices().ConfigureAwait(false);
Migrations.MigrationRunner.Run(appHost, _loggerFactory);
try
{
- await webHost.StartAsync(_tokenSource.Token).ConfigureAwait(false);
+ await host.StartAsync(_tokenSource.Token).ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
- var socketPath = GetUnixSocketPath(startupConfig, appPaths);
+ var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths);
- SetUnixSocketPermissions(startupConfig, socketPath);
+ StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger);
}
}
catch (Exception ex) when (ex is not TaskCanceledException)
{
- _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again.");
+ _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again");
throw;
}
await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
- _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(startTimestamp));
+ _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
// Block main thread until shutdown
await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
@@ -227,7 +238,7 @@ namespace Jellyfin.Server
}
catch (Exception ex)
{
- _logger.LogCritical(ex, "Error while starting server.");
+ _logger.LogCritical(ex, "Error while starting server");
}
finally
{
@@ -236,7 +247,7 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Running query planner optimizations in the database... This might take a while");
// Run before disposing the application
- var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false);
+ var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
if (context.Database.IsSqlite())
@@ -247,308 +258,7 @@ namespace Jellyfin.Server
}
await appHost.DisposeAsync().ConfigureAwait(false);
- }
-
- if (_restartOnShutdown)
- {
- StartNewInstance(options);
- }
- }
-
- /// <summary>
- /// Call static initialization methods for the application.
- /// </summary>
- public static void PerformStaticInitialization()
- {
- // Make sure we have all the code pages we can get
- // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
- Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
-
- // Increase the max http request limit
- // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
- ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
-
- // Disable the "Expect: 100-Continue" header by default
- // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
- ServicePointManager.Expect100Continue = false;
-
- Batteries_V2.Init();
- if (raw.sqlite3_enable_shared_cache(1) != raw.SQLITE_OK)
- {
- _logger.LogWarning("Failed to enable shared cache for SQLite");
- }
- }
-
- /// <summary>
- /// Configure the web host builder.
- /// </summary>
- /// <param name="builder">The builder to configure.</param>
- /// <param name="appHost">The application host.</param>
- /// <param name="serviceCollection">The application service collection.</param>
- /// <param name="commandLineOpts">The command line options passed to the application.</param>
- /// <param name="startupConfig">The application configuration.</param>
- /// <param name="appPaths">The application paths.</param>
- /// <returns>The configured web host builder.</returns>
- public static IWebHostBuilder ConfigureWebHostBuilder(
- this IWebHostBuilder builder,
- ApplicationHost appHost,
- IServiceCollection serviceCollection,
- StartupOptions commandLineOpts,
- IConfiguration startupConfig,
- IApplicationPaths appPaths)
- {
- return builder
- .UseKestrel((builderContext, options) =>
- {
- var addresses = appHost.NetManager.GetAllBindInterfaces();
-
- bool flagged = false;
- foreach (IPData netAdd in addresses)
- {
- _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd.Address);
- options.Listen(netAdd.Address, appHost.HttpPort);
- if (appHost.ListenWithHttps)
- {
- options.Listen(
- netAdd.Address,
- appHost.HttpsPort,
- listenOptions => listenOptions.UseHttps(appHost.Certificate));
- }
- else if (builderContext.HostingEnvironment.IsDevelopment())
- {
- try
- {
- options.Listen(
- netAdd.Address,
- appHost.HttpsPort,
- listenOptions => listenOptions.UseHttps());
- }
- catch (InvalidOperationException)
- {
- if (!flagged)
- {
- _logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
- flagged = true;
- }
- }
- }
- }
-
- // Bind to unix socket (only on unix systems)
- if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix)
- {
- var socketPath = GetUnixSocketPath(startupConfig, appPaths);
- options.ListenUnixSocket(socketPath);
- _logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
- }
- })
- .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
- .UseSerilog()
- .ConfigureServices(services =>
- {
- // Merge the external ServiceCollection into ASP.NET DI
- services.Add(serviceCollection);
- })
- .UseStartup<Startup>();
- }
-
- /// <summary>
- /// Create the data, config and log paths from the variety of inputs(command line args,
- /// environment variables) or decide on what default to use. For Windows it's %AppPath%
- /// for everything else the
- /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a>
- /// is followed.
- /// </summary>
- /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param>
- /// <returns><see cref="ServerApplicationPaths" />.</returns>
- private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
- {
- // dataDir
- // IF --datadir
- // ELSE IF $JELLYFIN_DATA_DIR
- // ELSE IF windows, use <%APPDATA%>/jellyfin
- // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin
- // ELSE use $HOME/.local/share/jellyfin
- var dataDir = options.DataDir;
- if (string.IsNullOrEmpty(dataDir))
- {
- dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR");
-
- if (string.IsNullOrEmpty(dataDir))
- {
- // LocalApplicationData follows the XDG spec on unix machines
- dataDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "jellyfin");
- }
- }
-
- // configDir
- // IF --configdir
- // ELSE IF $JELLYFIN_CONFIG_DIR
- // ELSE IF --datadir, use <datadir>/config (assume portable run)
- // ELSE IF <datadir>/config exists, use that
- // ELSE IF windows, use <datadir>/config
- // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin
- // ELSE $HOME/.config/jellyfin
- var configDir = options.ConfigDir;
- if (string.IsNullOrEmpty(configDir))
- {
- configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
-
- if (string.IsNullOrEmpty(configDir))
- {
- if (options.DataDir is not null
- || Directory.Exists(Path.Combine(dataDir, "config"))
- || OperatingSystem.IsWindows())
- {
- // Hang config folder off already set dataDir
- configDir = Path.Combine(dataDir, "config");
- }
- else
- {
- // $XDG_CONFIG_HOME defines the base directory relative to which
- // user specific configuration files should be stored.
- configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
-
- // If $XDG_CONFIG_HOME is either not set or empty,
- // a default equal to $HOME /.config should be used.
- if (string.IsNullOrEmpty(configDir))
- {
- configDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".config");
- }
-
- configDir = Path.Combine(configDir, "jellyfin");
- }
- }
- }
-
- // cacheDir
- // IF --cachedir
- // ELSE IF $JELLYFIN_CACHE_DIR
- // ELSE IF windows, use <datadir>/cache
- // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin
- // ELSE HOME/.cache/jellyfin
- var cacheDir = options.CacheDir;
- if (string.IsNullOrEmpty(cacheDir))
- {
- cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
-
- if (string.IsNullOrEmpty(cacheDir))
- {
- if (OperatingSystem.IsWindows())
- {
- // Hang cache folder off already set dataDir
- cacheDir = Path.Combine(dataDir, "cache");
- }
- else
- {
- // $XDG_CACHE_HOME defines the base directory relative to which
- // user specific non-essential data files should be stored.
- cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
-
- // If $XDG_CACHE_HOME is either not set or empty,
- // a default equal to $HOME/.cache should be used.
- if (string.IsNullOrEmpty(cacheDir))
- {
- cacheDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".cache");
- }
-
- cacheDir = Path.Combine(cacheDir, "jellyfin");
- }
- }
- }
-
- // webDir
- // IF --webdir
- // ELSE IF $JELLYFIN_WEB_DIR
- // ELSE <bindir>/jellyfin-web
- var webDir = options.WebDir;
- if (string.IsNullOrEmpty(webDir))
- {
- webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
-
- if (string.IsNullOrEmpty(webDir))
- {
- // Use default location under ResourcesPath
- webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web");
- }
- }
-
- // logDir
- // IF --logdir
- // ELSE IF $JELLYFIN_LOG_DIR
- // ELSE IF --datadir, use <datadir>/log (assume portable run)
- // ELSE <datadir>/log
- var logDir = options.LogDir;
- if (string.IsNullOrEmpty(logDir))
- {
- logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
-
- if (string.IsNullOrEmpty(logDir))
- {
- // Hang log folder off already set dataDir
- logDir = Path.Combine(dataDir, "log");
- }
- }
-
- // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
- dataDir = Path.GetFullPath(dataDir);
- logDir = Path.GetFullPath(logDir);
- configDir = Path.GetFullPath(configDir);
- cacheDir = Path.GetFullPath(cacheDir);
- webDir = Path.GetFullPath(webDir);
-
- // Ensure the main folders exist before we continue
- try
- {
- Directory.CreateDirectory(dataDir);
- Directory.CreateDirectory(logDir);
- Directory.CreateDirectory(configDir);
- Directory.CreateDirectory(cacheDir);
- }
- catch (IOException ex)
- {
- Console.Error.WriteLine("Error whilst attempting to create folder");
- Console.Error.WriteLine(ex.ToString());
- Environment.Exit(1);
- }
-
- return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
- }
-
- /// <summary>
- /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
- /// already.
- /// </summary>
- /// <param name="appPaths">The application paths.</param>
- /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns>
- public static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
- {
- // Do nothing if the config file already exists
- string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault);
- if (File.Exists(configPath))
- {
- return;
- }
-
- // Get a stream of the resource contents
- // NOTE: The .csproj name is used instead of the assembly name in the resource path
- const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
- Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
- ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
- await using (resource.ConfigureAwait(false))
- {
- Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- await using (dst.ConfigureAwait(false))
- {
- // Copy the resource contents to the expected file path for the config file
- await resource.CopyToAsync(dst).ConfigureAwait(false);
- }
+ host?.Dispose();
}
}
@@ -586,112 +296,5 @@ namespace Jellyfin.Server
.AddEnvironmentVariables("JELLYFIN_")
.AddInMemoryCollection(commandLineOpts.ConvertToConfig());
}
-
- /// <summary>
- /// Initialize Serilog using configuration and fall back to defaults on failure.
- /// </summary>
- private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
- {
- try
- {
- // Serilog.Log is used by SerilogLoggerFactory when no logger is specified
- Log.Logger = new LoggerConfiguration()
- .ReadFrom.Configuration(configuration)
- .Enrich.FromLogContext()
- .Enrich.WithThreadId()
- .CreateLogger();
- }
- catch (Exception ex)
- {
- Log.Logger = new LoggerConfiguration()
- .WriteTo.Console(
- outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}",
- formatProvider: CultureInfo.InvariantCulture)
- .WriteTo.Async(x => x.File(
- Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
- rollingInterval: RollingInterval.Day,
- outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
- formatProvider: CultureInfo.InvariantCulture,
- encoding: Encoding.UTF8))
- .Enrich.FromLogContext()
- .Enrich.WithThreadId()
- .CreateLogger();
-
- Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
- }
- }
-
- private static void StartNewInstance(StartupOptions options)
- {
- _logger.LogInformation("Starting new instance");
-
- var module = options.RestartPath;
-
- if (string.IsNullOrWhiteSpace(module))
- {
- module = Environment.GetCommandLineArgs()[0];
- }
-
- string commandLineArgsString;
- if (options.RestartArgs is not null)
- {
- commandLineArgsString = options.RestartArgs;
- }
- else
- {
- commandLineArgsString = string.Join(
- ' ',
- Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument));
- }
-
- _logger.LogInformation("Executable: {0}", module);
- _logger.LogInformation("Arguments: {0}", commandLineArgsString);
-
- Process.Start(module, commandLineArgsString);
- }
-
- private static string NormalizeCommandLineArgument(string arg)
- {
- if (!arg.Contains(' ', StringComparison.Ordinal))
- {
- return arg;
- }
-
- return "\"" + arg + "\"";
- }
-
- private static string GetUnixSocketPath(IConfiguration startupConfig, IApplicationPaths appPaths)
- {
- var socketPath = startupConfig.GetUnixSocketPath();
-
- if (string.IsNullOrEmpty(socketPath))
- {
- var xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
- var socketFile = "jellyfin.sock";
- if (xdgRuntimeDir is null)
- {
- // Fall back to config dir
- socketPath = Path.Join(appPaths.ConfigurationDirectoryPath, socketFile);
- }
- else
- {
- socketPath = Path.Join(xdgRuntimeDir, socketFile);
- }
- }
-
- return socketPath;
- }
-
- [UnsupportedOSPlatform("windows")]
- private static void SetUnixSocketPermissions(IConfiguration startupConfig, string socketPath)
- {
- var socketPerms = startupConfig.GetUnixSocketPermissions();
-
- if (!string.IsNullOrEmpty(socketPerms))
- {
- File.SetUnixFileMode(socketPath, (UnixFileMode)Convert.ToInt32(socketPerms, 8));
- _logger.LogInformation("Kestrel unix socket permissions set to {SocketPerms}", socketPerms);
- }
- }
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 49a57aa68..7abd2fbef 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -5,13 +5,14 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
+using Jellyfin.Api.Middleware;
using Jellyfin.MediaEncoding.Hls.Extensions;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Extensions;
+using Jellyfin.Server.HealthChecks;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Extensions;
using Jellyfin.Server.Infrastructure;
-using Jellyfin.Server.Middleware;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -34,20 +35,17 @@ namespace Jellyfin.Server
/// </summary>
public class Startup
{
- private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IServerApplicationHost _serverApplicationHost;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary>
- /// <param name="serverConfigurationManager">The server configuration manager.</param>
- /// <param name="serverApplicationHost">The server application host.</param>
- public Startup(
- IServerConfigurationManager serverConfigurationManager,
- IServerApplicationHost serverApplicationHost)
+ /// <param name="appHost">The server application host.</param>
+ public Startup(CoreAppHost appHost)
{
- _serverConfigurationManager = serverConfigurationManager;
- _serverApplicationHost = serverApplicationHost;
+ _serverApplicationHost = appHost;
+ _serverConfigurationManager = appHost.ConfigurationManager;
}
/// <summary>
@@ -86,8 +84,7 @@ namespace Jellyfin.Server
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
};
- services
- .AddHttpClient(NamedClient.Default, c =>
+ services.AddHttpClient(NamedClient.Default, c =>
{
c.DefaultRequestHeaders.UserAgent.Add(productHeader);
c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader);
@@ -122,7 +119,7 @@ namespace Jellyfin.Server
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
services.AddHealthChecks()
- .AddDbContextCheck<JellyfinDb>();
+ .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
services.AddHlsPlaylistGenerator();
}
@@ -207,7 +204,7 @@ namespace Jellyfin.Server
endpoints.MapControllers();
if (_serverConfigurationManager.Configuration.EnableMetrics)
{
- endpoints.MapMetrics("/metrics");
+ endpoints.MapMetrics();
}
endpoints.MapHealthChecks("/health");
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index 0d9f379e0..c3989751c 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -64,14 +64,6 @@ namespace Jellyfin.Server
public string? PackageName { get; set; }
/// <inheritdoc />
- [Option("restartpath", Required = false, HelpText = "Path to restart script.")]
- public string? RestartPath { get; set; }
-
- /// <inheritdoc />
- [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")]
- public string? RestartArgs { get; set; }
-
- /// <inheritdoc />
[Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")]
public string? PublishedServerUrl { get; set; }
diff --git a/Jellyfin.sln b/Jellyfin.sln
index 0514b9614..c0d2ec068 100644
--- a/Jellyfin.sln
+++ b/Jellyfin.sln
@@ -17,7 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.LocalMetadata", "MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj", "{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Drawing", "Emby.Drawing\Emby.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing", "src\Jellyfin.Drawing\Jellyfin.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}"
EndProject
@@ -42,7 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
SharedVersion.cs = SharedVersion.cs
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "src\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api", "Jellyfin.Api\Jellyfin.Api.csproj", "{DFBEFB4C-DA19-4143-98B7-27320C7F7163}"
EndProject
@@ -287,6 +287,8 @@ Global
{DA9FD356-4894-4830-B208-D6BCE3E65B11} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
{FE47334C-EFDE-4519-BD50-F24430FF360B} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
+ {154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 53683cdbd..96ee701b3 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -48,12 +48,6 @@ namespace MediaBrowser.Common
bool IsShuttingDown { get; }
/// <summary>
- /// Gets a value indicating whether this instance can self restart.
- /// </summary>
- /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
- bool CanSelfRestart { get; }
-
- /// <summary>
/// Gets the application version.
/// </summary>
/// <value>The application version.</value>
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 0296974b5..1b0ff27d9 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -49,7 +49,7 @@
<!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs
index 176bcbbd5..fa92d383a 100644
--- a/MediaBrowser.Common/Plugins/IPluginManager.cs
+++ b/MediaBrowser.Common/Plugins/IPluginManager.cs
@@ -30,6 +30,11 @@ namespace MediaBrowser.Common.Plugins
IEnumerable<Assembly> LoadAssemblies();
/// <summary>
+ /// Unloads all of the assemblies.
+ /// </summary>
+ void UnloadAssemblies();
+
+ /// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 49dd151f3..f2c2007f7 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -56,6 +56,7 @@ namespace MediaBrowser.Controller.Entities
".srt",
".vtt",
".sub",
+ ".sup",
".idx",
".txt",
".edl",
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index e586205c3..bccb4107f 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1300,8 +1300,15 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Adds the children to list.
/// </summary>
- private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query)
+ private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null)
{
+ // Prevent infinite recursion of nested folders
+ visitedFolders ??= new HashSet<Folder>();
+ if (!visitedFolders.Add(this))
+ {
+ return;
+ }
+
// If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
IEnumerable<BaseItem> children = null;
if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
@@ -1316,42 +1323,33 @@ namespace MediaBrowser.Controller.Entities
children = GetEligibleChildrenForRecursiveChildren(user);
}
- foreach (var child in children)
+ AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
+
+ if (includeLinkedChildren)
{
- bool? isVisibleToUser = null;
+ AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query, visitedFolders);
+ }
+ }
- if (query is null || UserViewBuilder.FilterItem(child, query))
+ private void AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders)
+ {
+ foreach (var child in children)
+ {
+ if (!child.IsVisible(user))
{
- isVisibleToUser = child.IsVisible(user);
-
- if (isVisibleToUser.Value)
- {
- result[child.Id] = child;
- }
+ continue;
}
- if (isVisibleToUser ?? child.IsVisible(user))
+ if (query is null || UserViewBuilder.FilterItem(child, query))
{
- if (recursive && child.IsFolder)
- {
- var folder = (Folder)child;
-
- folder.AddChildren(user, includeLinkedChildren, result, true, query);
- }
+ result[child.Id] = child;
}
- }
- if (includeLinkedChildren)
- {
- foreach (var child in GetLinkedChildren(user))
+ if (recursive && child.IsFolder)
{
- if (query is null || UserViewBuilder.FilterItem(child, query))
- {
- if (child.IsVisible(user))
- {
- result[child.Id] = child;
- }
- }
+ var folder = (Folder)child;
+
+ folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
}
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 3f30ac565..c83149a6d 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -320,7 +320,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!IsLocked)
{
- if (SourceType == SourceType.Library)
+ if (SourceType == SourceType.Library || SourceType == SourceType.LiveTV)
{
try
{
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 4a66edb16..20909c9d5 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -19,7 +19,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.2" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
</ItemGroup>
@@ -51,7 +51,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 7264c5eed..e94a04a7d 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -938,8 +938,10 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.SubtitleStream.IsExternal)
{
var subtitlePath = state.SubtitleStream.Path;
+ var subtitleExtension = Path.GetExtension(subtitlePath);
- if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase))
{
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
if (File.Exists(idxFile))
@@ -2127,15 +2129,30 @@ namespace MediaBrowser.Controller.MediaEncoding
var filters = new List<string>();
- // Boost volume to 200% when downsampling from 6ch to 2ch
if (channels.HasValue
- && channels.Value <= 2
+ && channels.Value == 2
&& state.AudioStream is not null
&& state.AudioStream.Channels.HasValue
- && state.AudioStream.Channels.Value > 5
- && !encodingOptions.DownMixAudioBoost.Equals(1))
+ && state.AudioStream.Channels.Value > 5)
{
- filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
+ switch (encodingOptions.DownMixStereoAlgorithm)
+ {
+ case DownMixStereoAlgorithms.Dave750:
+ filters.Add("volume=4.25");
+ filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
+ break;
+ case DownMixStereoAlgorithms.NightmodeDialogue:
+ filters.Add("pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5");
+ break;
+ case DownMixStereoAlgorithms.None:
+ default:
+ if (!encodingOptions.DownMixAudioBoost.Equals(1))
+ {
+ filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
+ }
+
+ break;
+ }
}
var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive;
@@ -5709,10 +5726,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return args;
}
- // Add the number of audio channels
var channels = state.OutputAudioChannels;
- if (channels.HasValue)
+ if (channels.HasValue && ((channels.Value != 2 && state.AudioStream.Channels <= 5) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))
{
args += " -ac " + channels.Value;
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index b16399598..eefc5d222 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -58,16 +58,6 @@ namespace MediaBrowser.Controller.Session
event EventHandler<SessionEventArgs> CapabilitiesChanged;
/// <summary>
- /// Occurs when [authentication failed].
- /// </summary>
- event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
-
- /// <summary>
- /// Occurs when [authentication succeeded].
- /// </summary>
- event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
-
- /// <summary>
/// Gets the sessions.
/// </summary>
/// <value>The sessions.</value>
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index de3987b1e..039127f9e 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -22,7 +22,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 91bf42b15..d95f894c5 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -498,11 +498,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogInformation("Starting {ProcessFileName} with args {ProcessArgs}", _ffprobePath, args);
+ var memoryStream = new MemoryStream();
+ await using (memoryStream.ConfigureAwait(false))
using (var processWrapper = new ProcessWrapper(process, this))
{
- await using var memoryStream = new MemoryStream();
StartProcess(processWrapper);
- await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken);
+ await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
memoryStream.Seek(0, SeekOrigin.Begin);
InternalMediaInfoResult result;
try
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 375041490..1233fb110 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -11,10 +11,6 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
- </PropertyGroup>
-
<ItemGroup>
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
@@ -27,7 +23,7 @@
<ItemGroup>
<PackageReference Include="BDInfo" Version="0.7.6.2" />
- <PackageReference Include="libse" Version="3.6.5" />
+ <PackageReference Include="libse" Version="3.6.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageReference Include="UTF.Unknown" Version="2.5.1" />
@@ -35,7 +31,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index b7c2fd7b1..90bc49132 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -226,7 +226,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, cancellationToken)
.ConfigureAwait(false);
- return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false);
+ return new SubtitleInfo()
+ {
+ Path = outputPath,
+ Protocol = MediaProtocol.File,
+ Format = outputFormat,
+ IsExternal = false
+ };
}
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
@@ -240,11 +246,23 @@ namespace MediaBrowser.MediaEncoding.Subtitles
await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
- return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
+ return new SubtitleInfo()
+ {
+ Path = outputPath,
+ Protocol = MediaProtocol.File,
+ Format = "srt",
+ IsExternal = true
+ };
}
// It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
- return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
+ return new SubtitleInfo()
+ {
+ Path = subtitleStream.Path,
+ Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path),
+ Format = currentFormat,
+ IsExternal = true
+ };
}
private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
@@ -728,23 +746,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- public readonly struct SubtitleInfo
+#pragma warning disable CA1034 // Nested types should not be visible
+ // Only public for the unit tests
+ public readonly record struct SubtitleInfo
{
- public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
- {
- Path = path;
- Protocol = protocol;
- Format = format;
- IsExternal = isExternal;
- }
-
- public string Path { get; }
+ public string Path { get; init; }
- public MediaProtocol Protocol { get; }
+ public MediaProtocol Protocol { get; init; }
- public string Format { get; }
+ public string Format { get; init; }
- public bool IsExternal { get; }
+ public bool IsExternal { get; init; }
}
}
}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index f4cd2f006..0ff95a2e1 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -1,128 +1,247 @@
#nullable disable
-#pragma warning disable CS1591
+using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Model.Configuration
+namespace MediaBrowser.Model.Configuration;
+
+/// <summary>
+/// Class EncodingOptions.
+/// </summary>
+public class EncodingOptions
{
- public class EncodingOptions
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EncodingOptions" /> class.
+ /// </summary>
+ public EncodingOptions()
{
- public EncodingOptions()
- {
- EnableFallbackFont = false;
- DownMixAudioBoost = 2;
- MaxMuxingQueueSize = 2048;
- EnableThrottling = false;
- ThrottleDelaySeconds = 180;
- EncodingThreadCount = -1;
- // This is a DRM device that is almost guaranteed to be there on every intel platform,
- // plus it's the default one in ffmpeg if you don't specify anything
- VaapiDevice = "/dev/dri/renderD128";
- EnableTonemapping = false;
- EnableVppTonemapping = false;
- TonemappingAlgorithm = "bt2390";
- TonemappingRange = "auto";
- TonemappingDesat = 0;
- TonemappingThreshold = 0.8;
- TonemappingPeak = 100;
- TonemappingParam = 0;
- VppTonemappingBrightness = 0;
- VppTonemappingContrast = 1.2;
- H264Crf = 23;
- H265Crf = 28;
- DeinterlaceDoubleRate = false;
- DeinterlaceMethod = "yadif";
- EnableDecodingColorDepth10Hevc = true;
- EnableDecodingColorDepth10Vp9 = true;
- EnableEnhancedNvdecDecoder = false;
- PreferSystemNativeHwDecoder = true;
- EnableIntelLowPowerH264HwEncoder = false;
- EnableIntelLowPowerHevcHwEncoder = false;
- EnableHardwareEncoding = true;
- AllowHevcEncoding = false;
- EnableSubtitleExtraction = true;
- AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
- HardwareDecodingCodecs = new string[] { "h264", "vc1" };
- }
-
- public int EncodingThreadCount { get; set; }
-
- public string TranscodingTempPath { get; set; }
-
- public string FallbackFontPath { get; set; }
-
- public bool EnableFallbackFont { get; set; }
-
- public double DownMixAudioBoost { get; set; }
-
- public int MaxMuxingQueueSize { get; set; }
-
- public bool EnableThrottling { get; set; }
-
- public int ThrottleDelaySeconds { get; set; }
-
- public string HardwareAccelerationType { get; set; }
-
- /// <summary>
- /// Gets or sets the FFmpeg path as set by the user via the UI.
- /// </summary>
- public string EncoderAppPath { get; set; }
-
- /// <summary>
- /// Gets or sets the current FFmpeg path being used by the system and displayed on the transcode page.
- /// </summary>
- public string EncoderAppPathDisplay { get; set; }
-
- public string VaapiDevice { get; set; }
-
- public bool EnableTonemapping { get; set; }
-
- public bool EnableVppTonemapping { get; set; }
-
- public string TonemappingAlgorithm { get; set; }
-
- public string TonemappingRange { get; set; }
-
- public double TonemappingDesat { get; set; }
-
- public double TonemappingThreshold { get; set; }
-
- public double TonemappingPeak { get; set; }
-
- public double TonemappingParam { get; set; }
-
- public double VppTonemappingBrightness { get; set; }
-
- public double VppTonemappingContrast { get; set; }
-
- public int H264Crf { get; set; }
-
- public int H265Crf { get; set; }
-
- public string EncoderPreset { get; set; }
-
- public bool DeinterlaceDoubleRate { get; set; }
-
- public string DeinterlaceMethod { get; set; }
-
- public bool EnableDecodingColorDepth10Hevc { get; set; }
-
- public bool EnableDecodingColorDepth10Vp9 { get; set; }
-
- public bool EnableEnhancedNvdecDecoder { get; set; }
-
- public bool PreferSystemNativeHwDecoder { get; set; }
-
- public bool EnableIntelLowPowerH264HwEncoder { get; set; }
-
- public bool EnableIntelLowPowerHevcHwEncoder { get; set; }
-
- public bool EnableHardwareEncoding { get; set; }
-
- public bool AllowHevcEncoding { get; set; }
-
- public bool EnableSubtitleExtraction { get; set; }
-
- public string[] HardwareDecodingCodecs { get; set; }
-
- public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; }
+ EnableFallbackFont = false;
+ DownMixAudioBoost = 2;
+ DownMixStereoAlgorithm = DownMixStereoAlgorithms.None;
+ MaxMuxingQueueSize = 2048;
+ EnableThrottling = false;
+ ThrottleDelaySeconds = 180;
+ EncodingThreadCount = -1;
+ // This is a DRM device that is almost guaranteed to be there on every intel platform,
+ // plus it's the default one in ffmpeg if you don't specify anything
+ VaapiDevice = "/dev/dri/renderD128";
+ EnableTonemapping = false;
+ EnableVppTonemapping = false;
+ TonemappingAlgorithm = "bt2390";
+ TonemappingRange = "auto";
+ TonemappingDesat = 0;
+ TonemappingThreshold = 0.8;
+ TonemappingPeak = 100;
+ TonemappingParam = 0;
+ VppTonemappingBrightness = 0;
+ VppTonemappingContrast = 1.2;
+ H264Crf = 23;
+ H265Crf = 28;
+ DeinterlaceDoubleRate = false;
+ DeinterlaceMethod = "yadif";
+ EnableDecodingColorDepth10Hevc = true;
+ EnableDecodingColorDepth10Vp9 = true;
+ EnableEnhancedNvdecDecoder = false;
+ PreferSystemNativeHwDecoder = true;
+ EnableIntelLowPowerH264HwEncoder = false;
+ EnableIntelLowPowerHevcHwEncoder = false;
+ EnableHardwareEncoding = true;
+ AllowHevcEncoding = false;
+ EnableSubtitleExtraction = true;
+ AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
+ HardwareDecodingCodecs = new string[] { "h264", "vc1" };
}
+
+ /// <summary>
+ /// Gets or sets the thread count used for encoding.
+ /// </summary>
+ public int EncodingThreadCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the temporary transcoding path.
+ /// </summary>
+ public string TranscodingTempPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path to the fallback font.
+ /// </summary>
+ public string FallbackFontPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to use the fallback font.
+ /// </summary>
+ public bool EnableFallbackFont { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio boost applied when downmixing audio.
+ /// </summary>
+ public double DownMixAudioBoost { get; set; }
+
+ /// <summary>
+ /// Gets or sets the algorithm used for downmixing audio to stereo.
+ /// </summary>
+ public DownMixStereoAlgorithms DownMixStereoAlgorithm { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum size of the muxing queue.
+ /// </summary>
+ public int MaxMuxingQueueSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether throttling is enabled.
+ /// </summary>
+ public bool EnableThrottling { get; set; }
+
+ /// <summary>
+ /// Gets or sets the delay after which throttling happens.
+ /// </summary>
+ public int ThrottleDelaySeconds { get; set; }
+
+ /// <summary>
+ /// Gets or sets the hardware acceleration type.
+ /// </summary>
+ public string HardwareAccelerationType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the FFmpeg path as set by the user via the UI.
+ /// </summary>
+ public string EncoderAppPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current FFmpeg path being used by the system and displayed on the transcode page.
+ /// </summary>
+ public string EncoderAppPathDisplay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the VA-API device.
+ /// </summary>
+ public string VaapiDevice { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether tonemapping is enabled.
+ /// </summary>
+ public bool EnableTonemapping { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether VPP tonemapping is enabled.
+ /// </summary>
+ public bool EnableVppTonemapping { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tone-mapping algorithm.
+ /// </summary>
+ public string TonemappingAlgorithm { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tone-mapping range.
+ /// </summary>
+ public string TonemappingRange { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tone-mapping desaturation.
+ /// </summary>
+ public double TonemappingDesat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tone-mapping threshold.
+ /// </summary>
+ public double TonemappingThreshold { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tone-mapping peak.
+ /// </summary>
+ public double TonemappingPeak { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tone-mapping parameters.
+ /// </summary>
+ public double TonemappingParam { get; set; }
+
+ /// <summary>
+ /// Gets or sets the VPP tone-mapping brightness.
+ /// </summary>
+ public double VppTonemappingBrightness { get; set; }
+
+ /// <summary>
+ /// Gets or sets the VPP tone-mapping contrast.
+ /// </summary>
+ public double VppTonemappingContrast { get; set; }
+
+ /// <summary>
+ /// Gets or sets the H264 CRF.
+ /// </summary>
+ public int H264Crf { get; set; }
+
+ /// <summary>
+ /// Gets or sets the H265 CRF.
+ /// </summary>
+ public int H265Crf { get; set; }
+
+ /// <summary>
+ /// Gets or sets the encoder preset.
+ /// </summary>
+ public string EncoderPreset { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the framerate is doubled when deinterlacing.
+ /// </summary>
+ public bool DeinterlaceDoubleRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the deinterlace method.
+ /// </summary>
+ public string DeinterlaceMethod { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether 10bit HEVC decoding is enabled.
+ /// </summary>
+ public bool EnableDecodingColorDepth10Hevc { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether 10bit VP9 decoding is enabled.
+ /// </summary>
+ public bool EnableDecodingColorDepth10Vp9 { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the enhanced NVDEC is enabled.
+ /// </summary>
+ public bool EnableEnhancedNvdecDecoder { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the system native hardware decoder should be used.
+ /// </summary>
+ public bool PreferSystemNativeHwDecoder { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the Intel H264 low-power hardware encoder should be used.
+ /// </summary>
+ public bool EnableIntelLowPowerH264HwEncoder { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the Intel HEVC low-power hardware encoder should be used.
+ /// </summary>
+ public bool EnableIntelLowPowerHevcHwEncoder { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether hardware encoding is enabled.
+ /// </summary>
+ public bool EnableHardwareEncoding { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether HEVC encoding is enabled.
+ /// </summary>
+ public bool AllowHevcEncoding { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether subtitle extraction is enabled.
+ /// </summary>
+ public bool EnableSubtitleExtraction { get; set; }
+
+ /// <summary>
+ /// Gets or sets the codecs hardware encoding is used for.
+ /// </summary>
+ public string[] HardwareDecodingCodecs { get; set; }
+
+ /// <summary>
+ /// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for.
+ /// </summary>
+ public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; }
}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index a07ab7121..c39162250 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -194,7 +194,7 @@ namespace MediaBrowser.Model.Configuration
public string[] CodecsUsed { get; set; } = Array.Empty<string>();
- public List<RepositoryInfo> PluginRepositories { get; set; } = new List<RepositoryInfo>();
+ public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty<RepositoryInfo>();
public bool EnableExternalContentInSuggestions { get; set; } = true;
@@ -259,5 +259,11 @@ namespace MediaBrowser.Model.Configuration
/// </summary>
/// <value>The chapter image resolution.</value>
public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource;
+
+ /// <summary>
+ /// Gets or sets the limit for parallel image encoding.
+ /// </summary>
+ /// <value>The limit for parallel image encoding.</value>
+ public int ParallelImageEncodingLimit { get; set; } = 0;
}
}
diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs
index df4018fdd..29aecf97f 100644
--- a/MediaBrowser.Model/Dlna/AudioOptions.cs
+++ b/MediaBrowser.Model/Dlna/MediaOptions.cs
@@ -1,5 +1,4 @@
#nullable disable
-#pragma warning disable CS1591
using System;
using MediaBrowser.Model.Dto;
@@ -7,11 +6,14 @@ using MediaBrowser.Model.Dto;
namespace MediaBrowser.Model.Dlna
{
/// <summary>
- /// Class AudioOptions.
+ /// Class MediaOptions.
/// </summary>
- public class AudioOptions
+ public class MediaOptions
{
- public AudioOptions()
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaOptions"/> class.
+ /// </summary>
+ public MediaOptions()
{
Context = EncodingContext.Streaming;
@@ -19,20 +21,49 @@ namespace MediaBrowser.Model.Dlna
EnableDirectStream = true;
}
+ /// <summary>
+ /// Gets or sets a value indicating whether direct playback is allowed.
+ /// </summary>
public bool EnableDirectPlay { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether direct streaming is allowed.
+ /// </summary>
public bool EnableDirectStream { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether direct playback is forced.
+ /// </summary>
public bool ForceDirectPlay { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether direct streaming is forced.
+ /// </summary>
public bool ForceDirectStream { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether audio stream copy is allowed.
+ /// </summary>
public bool AllowAudioStreamCopy { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether video stream copy is allowed.
+ /// </summary>
+ public bool AllowVideoStreamCopy { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item id.
+ /// </summary>
public Guid ItemId { get; set; }
+ /// <summary>
+ /// Gets or sets the media sources.
+ /// </summary>
public MediaSourceInfo[] MediaSources { get; set; }
+ /// <summary>
+ /// Gets or sets the device profile.
+ /// </summary>
public DeviceProfile Profile { get; set; }
/// <summary>
@@ -40,6 +71,9 @@ namespace MediaBrowser.Model.Dlna
/// </summary>
public string MediaSourceId { get; set; }
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
public string DeviceId { get; set; }
/// <summary>
@@ -49,7 +83,7 @@ namespace MediaBrowser.Model.Dlna
public int? MaxAudioChannels { get; set; }
/// <summary>
- /// Gets or sets the application's configured quality setting.
+ /// Gets or sets the application's configured maximum bitrate.
/// </summary>
public int? MaxBitrate { get; set; }
@@ -66,6 +100,16 @@ namespace MediaBrowser.Model.Dlna
public int? AudioTranscodingBitrate { get; set; }
/// <summary>
+ /// Gets or sets an override for the audio stream index.
+ /// </summary>
+ public int? AudioStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets an override for the subtitle stream index.
+ /// </summary>
+ public int? SubtitleStreamIndex { get; set; }
+
+ /// <summary>
/// Gets the maximum bitrate.
/// </summary>
/// <param name="isAudio">Whether or not this is audio.</param>
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 4c964c21a..bb41c9979 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -1,5 +1,4 @@
#nullable disable
-#pragma warning disable CS1591
using System;
using System.Collections.Generic;
@@ -13,6 +12,9 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Model.Dlna
{
+ /// <summary>
+ /// Class StreamBuilder.
+ /// </summary>
public class StreamBuilder
{
// Aliases
@@ -24,42 +26,56 @@ namespace MediaBrowser.Model.Dlna
private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
+ /// </summary>
+ /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/> object.</param>
+ /// <param name="logger">The <see cref="ILogger"/> object.</param>
public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
{
_transcoderSupport = transcoderSupport;
_logger = logger;
}
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger"/> object.</param>
public StreamBuilder(ILogger<StreamBuilder> logger)
: this(new FullTranscoderSupport(), logger)
{
}
- public StreamInfo BuildAudioItem(AudioOptions options)
+ /// <summary>
+ /// Gets the optimal audio stream.
+ /// </summary>
+ /// <param name="options">The <see cref="MediaOptions"/> object to get the audio stream from.</param>
+ /// <returns>The <see cref="StreamInfo"/> of the optimal audio stream.</returns>
+ public StreamInfo GetOptimalAudioStream(MediaOptions options)
{
- ValidateAudioInput(options);
+ ValidateMediaOptions(options, false);
var mediaSources = new List<MediaSourceInfo>();
- foreach (MediaSourceInfo i in options.MediaSources)
+ foreach (var mediaSource in options.MediaSources)
{
if (string.IsNullOrEmpty(options.MediaSourceId) ||
- string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+ string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
{
- mediaSources.Add(i);
+ mediaSources.Add(mediaSource);
}
}
var streams = new List<StreamInfo>();
- foreach (MediaSourceInfo i in mediaSources)
+ foreach (var mediaSourceInfo in mediaSources)
{
- StreamInfo streamInfo = BuildAudioItem(i, options);
+ StreamInfo streamInfo = GetOptimalAudioStream(mediaSourceInfo, options);
if (streamInfo is not null)
{
streams.Add(streamInfo);
}
}
- foreach (StreamInfo stream in streams)
+ foreach (var stream in streams)
{
stream.DeviceId = options.DeviceId;
stream.DeviceProfileId = options.Profile.Id;
@@ -68,31 +84,137 @@ namespace MediaBrowser.Model.Dlna
return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
}
- public StreamInfo BuildVideoItem(VideoOptions options)
+ private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
{
- ValidateInput(options);
+ var playlistItem = new StreamInfo
+ {
+ ItemId = options.ItemId,
+ MediaType = DlnaProfileType.Audio,
+ MediaSource = item,
+ RunTimeTicks = item.RunTimeTicks,
+ Context = options.Context,
+ DeviceProfile = options.Profile
+ };
+
+ if (options.ForceDirectPlay)
+ {
+ playlistItem.PlayMethod = PlayMethod.DirectPlay;
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
+ return playlistItem;
+ }
+
+ if (options.ForceDirectStream)
+ {
+ playlistItem.PlayMethod = PlayMethod.DirectStream;
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
+ return playlistItem;
+ }
+
+ MediaStream audioStream = item.GetDefaultAudioStream(null);
+
+ var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options);
+
+ var directPlayMethod = directPlayInfo.PlayMethod;
+ var transcodeReasons = directPlayInfo.TranscodeReasons;
+
+ var inputAudioChannels = audioStream?.Channels;
+ var inputAudioBitrate = audioStream?.BitDepth;
+ var inputAudioSampleRate = audioStream?.SampleRate;
+ var inputAudioBitDepth = audioStream?.BitDepth;
+
+ if (directPlayMethod.HasValue)
+ {
+ var profile = options.Profile;
+ var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
+ var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions);
+ transcodeReasons |= audioFailureReasons;
+
+ if (audioFailureReasons == 0)
+ {
+ playlistItem.PlayMethod = directPlayMethod.Value;
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile);
+
+ return playlistItem;
+ }
+ }
+
+ TranscodingProfile transcodingProfile = null;
+ foreach (var tcProfile in options.Profile.TranscodingProfiles)
+ {
+ if (tcProfile.Type == playlistItem.MediaType
+ && tcProfile.Context == options.Context
+ && _transcoderSupport.CanEncodeToAudioCodec(transcodingProfile.AudioCodec ?? tcProfile.Container))
+ {
+ transcodingProfile = tcProfile;
+ break;
+ }
+ }
+
+ if (transcodingProfile != null)
+ {
+ if (!item.SupportsTranscoding)
+ {
+ return null;
+ }
+
+ SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
+
+ var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray();
+ ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
+
+ // Honor requested max channels
+ playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
+
+ var configuredBitrate = options.GetMaxBitrate(true);
+
+ long transcodingBitrate = options.AudioTranscodingBitrate
+ ?? (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null)
+ ?? configuredBitrate
+ ?? 128000;
+
+ if (configuredBitrate.HasValue)
+ {
+ transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate);
+ }
+
+ var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
+ playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
+ }
+
+ playlistItem.TranscodeReasons = transcodeReasons;
+ return playlistItem;
+ }
+
+ /// <summary>
+ /// Gets the optimal video stream.
+ /// </summary>
+ /// <param name="options">The <see cref="MediaOptions"/> object to get the video stream from.</param>
+ /// <returns>The <see cref="StreamInfo"/> of the optimal video stream.</returns>
+ public StreamInfo GetOptimalVideoStream(MediaOptions options)
+ {
+ ValidateMediaOptions(options, true);
var mediaSources = new List<MediaSourceInfo>();
- foreach (MediaSourceInfo i in options.MediaSources)
+ foreach (var mediaSourceInfo in options.MediaSources)
{
if (string.IsNullOrEmpty(options.MediaSourceId) ||
- string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
+ string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
{
- mediaSources.Add(i);
+ mediaSources.Add(mediaSourceInfo);
}
}
var streams = new List<StreamInfo>();
- foreach (MediaSourceInfo i in mediaSources)
+ foreach (var mediaSourceInfo in mediaSources)
{
- var streamInfo = BuildVideoItem(i, options);
+ var streamInfo = BuildVideoItem(mediaSourceInfo, options);
if (streamInfo is not null)
{
streams.Add(streamInfo);
}
}
- foreach (StreamInfo stream in streams)
+ foreach (var stream in streams)
{
stream.DeviceId = options.DeviceId;
stream.DeviceProfileId = options.Profile.Id;
@@ -236,6 +358,14 @@ namespace MediaBrowser.Model.Dlna
}
}
+ /// <summary>
+ /// Normalizes input container.
+ /// </summary>
+ /// <param name="inputContainer">The input container.</param>
+ /// <param name="profile">The <see cref="DeviceProfile"/>.</param>
+ /// <param name="type">The <see cref="DlnaProfileType"/>.</param>
+ /// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
+ /// <returns>The the normalized input container.</returns>
public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null)
{
if (string.IsNullOrEmpty(inputContainer))
@@ -264,108 +394,7 @@ namespace MediaBrowser.Model.Dlna
return formats[0];
}
- private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
- {
- StreamInfo playlistItem = new StreamInfo
- {
- ItemId = options.ItemId,
- MediaType = DlnaProfileType.Audio,
- MediaSource = item,
- RunTimeTicks = item.RunTimeTicks,
- Context = options.Context,
- DeviceProfile = options.Profile
- };
-
- if (options.ForceDirectPlay)
- {
- playlistItem.PlayMethod = PlayMethod.DirectPlay;
- playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
- return playlistItem;
- }
-
- if (options.ForceDirectStream)
- {
- playlistItem.PlayMethod = PlayMethod.DirectStream;
- playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
- return playlistItem;
- }
-
- var audioStream = item.GetDefaultAudioStream(null);
-
- var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options);
-
- var directPlayMethod = directPlayInfo.PlayMethod;
- var transcodeReasons = directPlayInfo.TranscodeReasons;
-
- int? inputAudioChannels = audioStream?.Channels;
- int? inputAudioBitrate = audioStream?.BitDepth;
- int? inputAudioSampleRate = audioStream?.SampleRate;
- int? inputAudioBitDepth = audioStream?.BitDepth;
-
- if (directPlayMethod.HasValue)
- {
- var profile = options.Profile;
- var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
- var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions);
- transcodeReasons |= audioFailureReasons;
-
- if (audioFailureReasons == 0)
- {
- playlistItem.PlayMethod = directPlayMethod.Value;
- playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile);
-
- return playlistItem;
- }
- }
-
- TranscodingProfile transcodingProfile = null;
- foreach (var i in options.Profile.TranscodingProfiles)
- {
- if (i.Type == playlistItem.MediaType
- && i.Context == options.Context
- && _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container))
- {
- transcodingProfile = i;
- break;
- }
- }
-
- if (transcodingProfile is not null)
- {
- if (!item.SupportsTranscoding)
- {
- return null;
- }
-
- SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
-
- var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray();
- ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
-
- // Honor requested max channels
- playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
-
- var configuredBitrate = options.GetMaxBitrate(true);
-
- long transcodingBitrate = options.AudioTranscodingBitrate ??
- (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ??
- configuredBitrate ??
- 128000;
-
- if (configuredBitrate.HasValue)
- {
- transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate);
- }
-
- var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
- playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
- }
-
- playlistItem.TranscodeReasons = transcodeReasons;
- return playlistItem;
- }
-
- private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
+ private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
{
var directPlayProfile = options.Profile.DirectPlayProfiles
.FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
@@ -388,7 +417,7 @@ namespace MediaBrowser.Model.Dlna
// If device requirements are satisfied then allow both direct stream and direct play
if (item.SupportsDirectPlay)
{
- if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay))
+ if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
{
if (options.EnableDirectPlay)
{
@@ -404,7 +433,7 @@ namespace MediaBrowser.Model.Dlna
// While options takes the network and other factors into account. Only applies to direct stream
if (item.SupportsDirectStream)
{
- if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
+ if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
{
if (options.EnableDirectStream)
{
@@ -427,7 +456,6 @@ namespace MediaBrowser.Model.Dlna
var containerSupported = false;
var audioSupported = false;
var videoSupported = false;
- TranscodeReason reasons = 0;
foreach (var profile in directPlayProfiles)
{
@@ -447,6 +475,7 @@ namespace MediaBrowser.Model.Dlna
}
}
+ TranscodeReason reasons = 0;
if (!containerSupported)
{
reasons |= TranscodeReason.ContainerNotSupported;
@@ -547,7 +576,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile)
+ private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile)
{
var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
var protocol = "http";
@@ -562,7 +591,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
}
- private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
+ private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
{
ArgumentNullException.ThrowIfNull(item);
@@ -601,11 +630,15 @@ namespace MediaBrowser.Model.Dlna
var videoStream = item.VideoStream;
- var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
- var directStreamBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream);
- bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayBitrateEligibility == 0);
- bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamBitrateEligibility == 0);
- var transcodeReasons = directPlayBitrateEligibility | directStreamBitrateEligibility;
+ var bitrateLimitExceeded = IsBitrateLimitExceeded(item, options.GetMaxBitrate(false) ?? 0);
+ var isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || !bitrateLimitExceeded);
+ var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded);
+ TranscodeReason transcodeReasons = 0;
+
+ if (bitrateLimitExceeded)
+ {
+ transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit;
+ }
_logger.LogDebug(
"Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
@@ -702,7 +735,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- _logger.LogInformation(
+ _logger.LogDebug(
"StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}",
options.Profile.Name ?? "Anonymous Profile",
item.Path ?? "Unknown path",
@@ -716,7 +749,7 @@ namespace MediaBrowser.Model.Dlna
return playlistItem;
}
- private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem)
+ private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem)
{
if (!(item.SupportsTranscoding || item.SupportsDirectStream))
{
@@ -763,7 +796,7 @@ namespace MediaBrowser.Model.Dlna
return transcodingProfiles.FirstOrDefault();
}
- private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
+ private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
{
// Prefer matching video codecs
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
@@ -867,7 +900,7 @@ namespace MediaBrowser.Model.Dlna
// Honor requested max channels
playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
- int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
+ int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
@@ -882,14 +915,14 @@ namespace MediaBrowser.Model.Dlna
i.ContainsAnyCodec(audioCodec, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
isFirstAppliedCodecProfile = true;
- foreach (var i in appliedAudioConditions)
+ foreach (var codecProfile in appliedAudioConditions)
{
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
foreach (var transcodingAudioCodec in transcodingAudioCodecs)
{
- if (i.ContainsAnyCodec(transcodingAudioCodec, container))
+ if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
{
- ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
+ ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
isFirstAppliedCodecProfile = false;
break;
}
@@ -1050,7 +1083,7 @@ namespace MediaBrowser.Model.Dlna
}
private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
- VideoOptions options,
+ MediaOptions options,
MediaSourceInfo mediaSource,
MediaStream videoStream,
MediaStream audioStream,
@@ -1237,7 +1270,7 @@ namespace MediaBrowser.Model.Dlna
return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
}
- private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream)
+ private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream)
{
var profile = options.Profile;
var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
@@ -1274,23 +1307,17 @@ namespace MediaBrowser.Model.Dlna
mediaSource.Path ?? "Unknown path");
}
- private TranscodeReason IsBitrateEligibleForDirectPlayback(
- MediaSourceInfo item,
- long maxBitrate,
- VideoOptions options,
- PlayMethod playMethod)
- {
- bool result = IsItemBitrateEligibleForDirectPlayback(item, maxBitrate, playMethod);
- if (!result)
- {
- return TranscodeReason.ContainerBitrateExceedsLimit;
- }
- else
- {
- return 0;
- }
- }
-
+ /// <summary>
+ /// Normalizes input container.
+ /// </summary>
+ /// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
+ /// <param name="subtitleStream">The <see cref="MediaStream"/> of the subtitle stream.</param>
+ /// <param name="subtitleProfiles">The list of supported <see cref="SubtitleProfile"/>s.</param>
+ /// <param name="playMethod">The <see cref="PlayMethod"/>.</param>
+ /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
+ /// <param name="outputContainer">The output container.</param>
+ /// <param name="transcodingSubProtocol">The subtitle transoding protocol.</param>
+ /// <returns>The the normalized input container.</returns>
public static SubtitleProfile GetSubtitleProfile(
MediaSourceInfo mediaSource,
MediaStream subtitleStream,
@@ -1448,12 +1475,12 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
+ private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate)
{
// Don't restrict bitrate if item is remote.
if (item.IsRemote)
{
- return true;
+ return false;
}
// If no maximum bitrate is set, default to no maximum bitrate.
@@ -1465,40 +1492,22 @@ namespace MediaBrowser.Model.Dlna
if (itemBitrate > requestedMaxBitrate)
{
_logger.LogDebug(
- "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
- playMethod,
+ "Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
itemBitrate,
requestedMaxBitrate);
- return false;
- }
-
- return true;
- }
-
- private static void ValidateInput(VideoOptions options)
- {
- ValidateAudioInput(options);
-
- if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
- {
- throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
+ return true;
}
- if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
- {
- throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
- }
+ return false;
}
- private static void ValidateAudioInput(AudioOptions options)
+ private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource)
{
if (options.ItemId.Equals(default))
{
- throw new ArgumentException("ItemId is required");
+ ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
}
- ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
-
if (options.Profile is null)
{
throw new ArgumentException("Profile is required");
@@ -1508,6 +1517,19 @@ namespace MediaBrowser.Model.Dlna
{
throw new ArgumentException("MediaSources is required");
}
+
+ if (isMediaSource)
+ {
+ if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
+ {
+ throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
+ }
+
+ if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
+ {
+ throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
+ }
+ }
}
private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio(
@@ -1825,8 +1847,8 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- // change from split by | to comma
- // strip spaces to avoid having to encode
+ // Change from split by | to comma
+ // Strip spaces to avoid having to encode
var values = value
.Split('|', StringSplitOptions.RemoveEmptyEntries);
diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs
deleted file mode 100644
index 0cb80af54..000000000
--- a/MediaBrowser.Model/Dlna/VideoOptions.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Dlna
-{
- /// <summary>
- /// Class VideoOptions.
- /// </summary>
- public class VideoOptions : AudioOptions
- {
- public int? AudioStreamIndex { get; set; }
-
- public int? SubtitleStreamIndex { get; set; }
-
- public bool AllowVideoStreamCopy { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs
new file mode 100644
index 000000000..385cd6a34
--- /dev/null
+++ b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// An enum representing an algorithm to downmix 6ch+ to stereo.
+/// Algorithms sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620.
+/// </summary>
+public enum DownMixStereoAlgorithms
+{
+ /// <summary>
+ /// No special algorithm.
+ /// </summary>
+ None = 0,
+
+ /// <summary>
+ /// Algorithm by Dave_750.
+ /// </summary>
+ Dave750 = 1,
+
+ /// <summary>
+ /// Nightmode Dialogue algorithm.
+ /// </summary>
+ NightmodeDialogue = 2
+}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 344ebaf80..47341f4e1 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -635,11 +635,12 @@ namespace MediaBrowser.Model.Entities
// sub = external .sub file
- return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) &&
- !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase) &&
- !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) &&
- !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase) &&
- !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase);
+ return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
+ && !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase)
+ && !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase);
}
public bool SupportsSubtitleConversionTo(string toCodec)
diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
index a5a6b18aa..c6d1f3900 100644
--- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
+++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
@@ -24,24 +24,27 @@ namespace MediaBrowser.Model.Extensions
requestedLanguage = "en";
}
- var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase);
-
return remoteImageInfos.OrderByDescending(i =>
{
+ // Image priority ordering:
+ // - Images that match the requested language
+ // - Images with no language
+ // - TODO: Images that match the original language
+ // - Images in English
+ // - Images that don't match the requested language
+
if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
{
- return 3;
+ return 4;
}
if (string.IsNullOrEmpty(i.Language))
{
- // Assume empty image language is likely to be English.
- return isRequestedLanguageEn ? 3 : 2;
+ return 3;
}
- if (!isRequestedLanguageEn && string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase))
{
- // Prioritize English over non-requested languages.
return 2;
}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs
index 4cece941c..25e5c7796 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs
@@ -40,5 +40,9 @@ namespace MediaBrowser.Model.LiveTv
public string RecordingPostProcessor { get; set; }
public string RecordingPostProcessorArguments { get; set; }
+
+ public bool SaveRecordingNFO { get; set; } = true;
+
+ public bool SaveRecordingImages { get; set; } = true;
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 284e89f1c..521ba0f10 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -49,7 +49,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index a82c1c8c0..9e56849c7 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -79,8 +79,9 @@ namespace MediaBrowser.Model.System
/// <summary>
/// Gets or sets a value indicating whether this instance can self restart.
/// </summary>
- /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
- public bool CanSelfRestart { get; set; }
+ /// <value><c>true</c>.</value>
+ [Obsolete("This is always true")]
+ public bool CanSelfRestart { get; set; } = true;
public bool CanLaunchWebBrowser { get; set; }
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index 50e704060..e7c2cd255 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -264,7 +264,8 @@ namespace MediaBrowser.Providers.Manager
var fileStreamOptions = AsyncFile.WriteOptions;
fileStreamOptions.Mode = FileMode.Create;
fileStreamOptions.PreallocationSize = source.Length;
- await using (var fs = new FileStream(path, fileStreamOptions))
+ var fs = new FileStream(path, fileStreamOptions);
+ await using (fs.ConfigureAwait(false))
{
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index a0f48840e..d621555f1 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -502,15 +502,17 @@ namespace MediaBrowser.Providers.Manager
break;
}
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- await _providerManager.SaveImage(
- item,
- stream,
- response.Content.Headers.ContentType?.MediaType,
- type,
- null,
- cancellationToken).ConfigureAwait(false);
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
+ {
+ await _providerManager.SaveImage(
+ item,
+ stream,
+ response.Content.Headers.ContentType?.MediaType,
+ type,
+ null,
+ cancellationToken).ConfigureAwait(false);
+ }
result.UpdateType |= ItemUpdateType.ImageUpdate;
return true;
@@ -626,14 +628,18 @@ namespace MediaBrowser.Providers.Manager
}
}
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await _providerManager.SaveImage(
- item,
- stream,
- response.Content.Headers.ContentType?.MediaType,
- imageType,
- null,
- cancellationToken).ConfigureAwait(false);
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
+ {
+ await _providerManager.SaveImage(
+ item,
+ stream,
+ response.Content.Headers.ContentType?.MediaType,
+ imageType,
+ null,
+ cancellationToken).ConfigureAwait(false);
+ }
+
result.UpdateType |= ItemUpdateType.ImageUpdate;
}
catch (HttpRequestException)
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index ff06c7ce4..ffae77200 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -444,8 +444,8 @@ namespace MediaBrowser.Providers.Manager
}
}
- if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue) ||
- (originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
+ if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue)
+ || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
{
updateType |= ItemUpdateType.MetadataEdit;
}
@@ -685,7 +685,8 @@ namespace MediaBrowser.Providers.Manager
{
try
{
- if (!options.IsReplacingImage(remoteImage.Type))
+ if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
+ && !options.IsReplacingImage(remoteImage.Type))
{
continue;
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 914da33a9..0ce696edc 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -182,14 +182,17 @@ namespace MediaBrowser.Providers.Manager
contentType = MimeTypes.GetMimeType(url);
}
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await SaveImage(
- item,
- stream,
- contentType,
- type,
- imageIndex,
- cancellationToken).ConfigureAwait(false);
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
+ {
+ await SaveImage(
+ item,
+ stream,
+ contentType,
+ type,
+ imageIndex,
+ cancellationToken).ConfigureAwait(false);
+ }
}
/// <inheritdoc/>
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index dbacc2a82..13de86a92 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -24,7 +24,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="PlaylistsNET" Version="1.3.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
- <PackageReference Include="TMDbLib" Version="1.9.2" />
+ <PackageReference Include="TMDbLib" Version="2.0.0" />
</ItemGroup>
<PropertyGroup>
@@ -34,13 +34,9 @@
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
- </PropertyGroup>
-
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
index fed23df15..f58f5f7a3 100644
--- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
@@ -140,7 +140,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (attachmentStream is not null)
{
- return await ExtractAttachment(item, attachmentStream, mediaSource, cancellationToken);
+ return await ExtractAttachment(item, attachmentStream, mediaSource, cancellationToken).ConfigureAwait(false);
}
// Fall back to EmbeddedImage streams
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 751135a2c..81434b862 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -557,7 +557,7 @@ namespace MediaBrowser.Providers.MediaInfo
CancellationToken cancellationToken)
{
var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
- var externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken);
+ var externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken).ConfigureAwait(false);
var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default ||
options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
@@ -611,7 +611,7 @@ namespace MediaBrowser.Providers.MediaInfo
// Rescan
if (downloadedLanguages.Count > 0)
{
- externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken);
+ externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
index 7fb438d8a..7f73afc53 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
@@ -42,11 +43,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType>
- {
- ImageType.Primary,
- ImageType.Disc
- };
+ yield return ImageType.Primary;
+ yield return ImageType.Disc;
}
/// <inheritdoc />
@@ -60,16 +58,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = AsyncFile.OpenRead(path);
- var obj = await JsonSerializer.DeserializeAsync<AudioDbAlbumProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
- if (obj is not null && obj.album is not null && obj.album.Count > 0)
+ FileStream jsonStream = AsyncFile.OpenRead(path);
+ await using (jsonStream.ConfigureAwait(false))
{
- return GetImages(obj.album[0]);
+ var obj = await JsonSerializer.DeserializeAsync<AudioDbAlbumProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+
+ if (obj is not null && obj.album is not null && obj.album.Count > 0)
+ {
+ return GetImages(obj.album[0]);
+ }
}
}
- return new List<RemoteImageInfo>();
+ return Enumerable.Empty<RemoteImageInfo>();
}
private IEnumerable<RemoteImageInfo> GetImages(AudioDbAlbumProvider.Album item)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
index b92f1f59f..55e2474a5 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
@@ -68,14 +68,17 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetAlbumInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = AsyncFile.OpenRead(path);
- var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
- if (obj is not null && obj.album is not null && obj.album.Count > 0)
+ FileStream jsonStream = AsyncFile.OpenRead(path);
+ await using (jsonStream.ConfigureAwait(false))
{
- result.Item = new MusicAlbum();
- result.HasMetadata = true;
- ProcessResult(result.Item, obj.album[0], info.MetadataLanguage);
+ var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+
+ if (obj is not null && obj.album is not null && obj.album.Count > 0)
+ {
+ result.Item = new MusicAlbum();
+ result.HasMetadata = true;
+ ProcessResult(result.Item, obj.album[0], info.MetadataLanguage);
+ }
}
}
@@ -173,13 +176,18 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- var fileStreamOptions = AsyncFile.WriteOptions;
- fileStreamOptions.Mode = FileMode.Create;
- fileStreamOptions.PreallocationSize = stream.Length;
- await using var xmlFileStream = new FileStream(path, fileStreamOptions);
- await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
+ {
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = stream.Length;
+ var xmlFileStream = new FileStream(path, fileStreamOptions);
+ await using (xmlFileStream.ConfigureAwait(false))
+ {
+ await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
+ }
+ }
}
private static string GetAlbumDataPath(IApplicationPaths appPaths, string musicBrainzReleaseGroupId)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
index 6d67ad634..b1a285a96 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
@@ -62,12 +62,15 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = AsyncFile.OpenRead(path);
- var obj = await JsonSerializer.DeserializeAsync<AudioDbArtistProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
- if (obj is not null && obj.artists is not null && obj.artists.Count > 0)
+ FileStream jsonStream = AsyncFile.OpenRead(path);
+ await using (jsonStream.ConfigureAwait(false))
{
- return GetImages(obj.artists[0]);
+ var obj = await JsonSerializer.DeserializeAsync<AudioDbArtistProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+
+ if (obj is not null && obj.artists is not null && obj.artists.Count > 0)
+ {
+ return GetImages(obj.artists[0]);
+ }
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index 1565a8c51..f3385b3a9 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -67,14 +67,17 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetArtistInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = AsyncFile.OpenRead(path);
- var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
- if (obj is not null && obj.artists is not null && obj.artists.Count > 0)
+ FileStream jsonStream = AsyncFile.OpenRead(path);
+ await using (jsonStream.ConfigureAwait(false))
{
- result.Item = new MusicArtist();
- result.HasMetadata = true;
- ProcessResult(result.Item, obj.artists[0], info.MetadataLanguage);
+ var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+
+ if (obj is not null && obj.artists is not null && obj.artists.Count > 0)
+ {
+ result.Item = new MusicArtist();
+ result.HasMetadata = true;
+ ProcessResult(result.Item, obj.artists[0], info.MetadataLanguage);
+ }
}
}
@@ -151,16 +154,21 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- var fileStreamOptions = AsyncFile.WriteOptions;
- fileStreamOptions.Mode = FileMode.Create;
- fileStreamOptions.PreallocationSize = stream.Length;
- await using var xmlFileStream = new FileStream(path, fileStreamOptions);
- await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
+ {
+ var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = stream.Length;
+ var xmlFileStream = new FileStream(path, fileStreamOptions);
+ await using (xmlFileStream.ConfigureAwait(false))
+ {
+ await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
+ }
+ }
}
/// <summary>
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 3ef94ca93..e4bb4eaea 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -137,29 +137,31 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var url = OmdbProvider.GetOmdbUrl(urlQuery.ToString());
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- if (isSearch)
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
{
- var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (searchResultList?.Search is not null)
+ if (isSearch)
{
- var resultCount = searchResultList.Search.Count;
- var result = new RemoteSearchResult[resultCount];
- for (var i = 0; i < resultCount; i++)
+ var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (searchResultList?.Search is not null)
{
- result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd);
+ var resultCount = searchResultList.Search.Count;
+ var result = new RemoteSearchResult[resultCount];
+ for (var i = 0; i < resultCount; i++)
+ {
+ result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd);
+ }
+
+ return result;
}
-
- return result;
}
- }
- else
- {
- var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase))
+ else
{
- return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) };
+ var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase))
+ {
+ return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) };
+ }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 6713a34e6..497437bd8 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -234,15 +234,21 @@ namespace MediaBrowser.Providers.Plugins.Omdb
internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
{
var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
- await using var stream = AsyncFile.OpenRead(path);
- return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var stream = AsyncFile.OpenRead(path);
+ await using (stream.ConfigureAwait(false))
+ {
+ return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
}
internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken)
{
var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
- await using var stream = AsyncFile.OpenRead(path);
- return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var stream = AsyncFile.OpenRead(path);
+ await using (stream.ConfigureAwait(false))
+ {
+ return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
}
/// <summary>Gets OMDB URL.</summary>
@@ -317,8 +323,11 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam));
var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<RootObject>(url, _jsonOptions, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await using (jsonFileStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
return path;
}
@@ -357,8 +366,11 @@ namespace MediaBrowser.Providers.Plugins.Omdb
seasonId));
var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<SeasonRootObject>(url, _jsonOptions, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await using (jsonFileStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
return path;
}
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index 4ff9e0247..0fb9d30a6 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -138,9 +138,15 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
Directory.CreateDirectory(Path.GetDirectoryName(file));
- await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
+ var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
+ await using (response.ConfigureAwait(false))
+ {
+ var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await using (fileStream.ConfigureAwait(false))
+ {
+ await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
+ }
+ }
}
return file;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index 20898d213..eee3658de 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -81,8 +81,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
var backdrops = collection.Images.Backdrops;
var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
- _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
- _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
+ remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
+ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
return remoteImages;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index 01b8bca39..02601d3f5 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -100,9 +100,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var logos = movie.Images.Logos;
var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count);
- _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
- _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
- _tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language, remoteImages);
+ remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
+ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
+ remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
return remoteImages;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
index aa46d8f25..bc959ee2b 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -69,12 +69,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
return Enumerable.Empty<RemoteImageInfo>();
}
- var profiles = personResult.Images.Profiles;
- var remoteImages = new List<RemoteImageInfo>(profiles.Count);
-
- _tmdbClientManager.ConvertProfilesToRemoteImageInfo(profiles, language, remoteImages);
-
- return remoteImages;
+ return _tmdbClientManager.ConvertProfilesToRemoteImageInfo(personResult.Images.Profiles, language);
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index 127d41cc7..5259faf76 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -89,11 +89,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new List<RemoteImageInfo>(stills.Count);
-
- _tmdbClientManager.ConvertStillsToRemoteImageInfo(stills, language, remoteImages);
-
- return remoteImages;
+ return _tmdbClientManager.ConvertStillsToRemoteImageInfo(stills, language);
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index fda00537d..b8d1460db 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -80,11 +80,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new List<RemoteImageInfo>(posters.Count);
-
- _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
-
- return remoteImages;
+ return _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language);
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 9062f1b85..79cb6e86d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -83,9 +83,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var logos = series.Images.Logos;
var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count);
- _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
- _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
- _tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language, remoteImages);
+ remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
+ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
+ remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
return remoteImages;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index b56c0d748..c7441bf35 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -531,55 +531,45 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="images">The input images.</param>
/// <param name="requestLanguage">The requested language.</param>
- /// <param name="results">The collection to add the remote images into.</param>
- public void ConvertPostersToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
- {
- ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLanguage, results);
- }
+ /// <returns>The remote images.</returns>
+ public IEnumerable<RemoteImageInfo> ConvertPostersToRemoteImageInfo(IReadOnlyList<ImageData> images, string requestLanguage)
+ => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLanguage);
/// <summary>
/// Converts backdrop <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
/// </summary>
/// <param name="images">The input images.</param>
/// <param name="requestLanguage">The requested language.</param>
- /// <param name="results">The collection to add the remote images into.</param>
- public void ConvertBackdropsToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
- {
- ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestLanguage, results);
- }
+ /// <returns>The remote images.</returns>
+ public IEnumerable<RemoteImageInfo> ConvertBackdropsToRemoteImageInfo(IReadOnlyList<ImageData> images, string requestLanguage)
+ => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestLanguage);
/// <summary>
/// Converts logo <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
/// </summary>
/// <param name="images">The input images.</param>
/// <param name="requestLanguage">The requested language.</param>
- /// <param name="results">The collection to add the remote images into.</param>
- public void ConvertLogosToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
- {
- ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.LogoSize, ImageType.Logo, requestLanguage, results);
- }
+ /// <returns>The remote images.</returns>
+ public IEnumerable<RemoteImageInfo> ConvertLogosToRemoteImageInfo(IReadOnlyList<ImageData> images, string requestLanguage)
+ => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.LogoSize, ImageType.Logo, requestLanguage);
/// <summary>
/// Converts profile <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
/// </summary>
/// <param name="images">The input images.</param>
/// <param name="requestLanguage">The requested language.</param>
- /// <param name="results">The collection to add the remote images into.</param>
- public void ConvertProfilesToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
- {
- ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLanguage, results);
- }
+ /// <returns>The remote images.</returns>
+ public IEnumerable<RemoteImageInfo> ConvertProfilesToRemoteImageInfo(IReadOnlyList<ImageData> images, string requestLanguage)
+ => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLanguage);
/// <summary>
/// Converts still <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
/// </summary>
/// <param name="images">The input images.</param>
/// <param name="requestLanguage">The requested language.</param>
- /// <param name="results">The collection to add the remote images into.</param>
- public void ConvertStillsToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
- {
- ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLanguage, results);
- }
+ /// <returns>The remote images.</returns>
+ public IEnumerable<RemoteImageInfo> ConvertStillsToRemoteImageInfo(IReadOnlyList<ImageData> images, string requestLanguage)
+ => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLanguage);
/// <summary>
/// Converts <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
@@ -588,8 +578,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="size">The size of the image to fetch.</param>
/// <param name="type">The type of the image.</param>
/// <param name="requestLanguage">The requested language.</param>
- /// <param name="results">The collection to add the remote images into.</param>
- private void ConvertToRemoteImageInfo(List<ImageData> images, string size, ImageType type, string requestLanguage, List<RemoteImageInfo> results)
+ /// <returns>The remote images.</returns>
+ private IEnumerable<RemoteImageInfo> ConvertToRemoteImageInfo(IReadOnlyList<ImageData> images, string size, ImageType type, string requestLanguage)
{
// sizes provided are for original resolution, don't store them when downloading scaled images
var scaleImage = !string.Equals(size, "original", StringComparison.OrdinalIgnoreCase);
@@ -598,7 +588,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
{
var image = images[i];
- results.Add(new RemoteImageInfo
+ yield return new RemoteImageInfo
{
Url = GetUrl(size, image.FilePath),
CommunityRating = image.VoteAverage,
@@ -609,7 +599,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
ProviderName = TmdbUtils.ProviderName,
Type = type,
RatingType = RatingType.Score
- });
+ };
}
}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 1aeffb65f..b1a26cfba 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -188,10 +188,16 @@ namespace MediaBrowser.Providers.Subtitles
{
var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
- await using var stream = response.Stream;
- await using var memoryStream = new MemoryStream();
- await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
- memoryStream.Position = 0;
+ var memoryStream = new MemoryStream();
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ var stream = response.Stream;
+ await using (stream.ConfigureAwait(false))
+ {
+ await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+ memoryStream.Position = 0;
+ }
+ }
var savePaths = new List<string>();
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 6e82d96d1..c25932a5a 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -22,7 +22,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/README.md b/README.md
index 1963ba526..2362741b4 100644
--- a/README.md
+++ b/README.md
@@ -47,16 +47,16 @@
Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest!
-For further details, please see [our documentation page](https://docs.jellyfin.org/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://docs.jellyfin.org/general/getting-help.html). For more information about the project, please see our [about page](https://docs.jellyfin.org/general/about.html).
+For further details, please see [our documentation page](https://jellyfin.org/docs/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://jellyfin.org/docs/general/getting-help). For more information about the project, please see our [about page](https://jellyfin.org/docs/general/about).
<strong>Want to get started?</strong><br/>
-Check out our <a href="https://jellyfin.org/downloads">downloads page</a> or our <a href="https://docs.jellyfin.org/general/administration/installing.html">installation guide</a>, then see our <a href="https://docs.jellyfin.org/general/quick-start.html">quick start guide</a>. You can also <a href="https://docs.jellyfin.org/general/administration/building.html">build from source</a>.<br/>
+Check out our <a href="https://jellyfin.org/downloads">downloads page</a> or our <a href="https://jellyfin.org/docs/general/installation/">installation guide</a>, then see our <a href="https://jellyfin.org/docs/general/quick-start">quick start guide</a>. You can also <a href="https://jellyfin.org/docs/general/installation/source">build from source</a>.<br/>
<strong>Something not working right?</strong><br/>
-Open an <a href="https://docs.jellyfin.org/general/contributing/issues.html">Issue</a> on GitHub.<br/>
+Open an <a href="https://jellyfin.org/docs/general/contributing/issues">Issue</a> on GitHub.<br/>
<strong>Want to contribute?</strong><br/>
-Check out our <a href="https://jellyfin.org/contribute">contributing choose-your-own-adventure</a> to see where you can help, then see our <a href="https://docs.jellyfin.org/general/contributing/index.html">contributing guide</a> and our <a href="https://jellyfin.org/docs/general/community-standards">community standards</a>.<br/>
+Check out our <a href="https://jellyfin.org/contribute">contributing choose-your-own-adventure</a> to see where you can help, then see our <a href="https://jellyfin.org/docs/general/contributing/">contributing guide</a> and our <a href="https://jellyfin.org/docs/general/community-standards">community standards</a>.<br/>
<strong>New idea or improvement?</strong><br/>
Check out our <a href="https://features.jellyfin.org/?view=most-wanted">feature request hub</a>.<br/>
diff --git a/debian/postinst b/debian/postinst
index 47173855f..a15442c76 100644
--- a/debian/postinst
+++ b/debian/postinst
@@ -10,6 +10,8 @@ if [[ -f $DEFAULT_FILE ]]; then
fi
JELLYFIN_USER=${JELLYFIN_USER:-jellyfin}
+RENDER_GROUP=${RENDER_GROUP:-render}
+VIDEO_GROUP=${VIDEO_GROUP:-video}
# Data directories for program data (cache, db), configs, and logs
PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
@@ -28,6 +30,14 @@ case "$1" in
adduser --system --ingroup ${JELLYFIN_USER} --shell /bin/false ${JELLYFIN_USER} --no-create-home --home ${PROGRAMDATA} \
--gecos "Jellyfin default user" > /dev/null 2>&1
fi
+ # add jellyfin to the render group for hwa
+ if [[ ! -z "$(getent group ${RENDER_GROUP})" ]]; then
+ usermod -aG ${RENDER_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1
+ fi
+ # add jellyfin to the video group for hwa
+ if [[ ! -z "$(getent group ${VIDEO_GROUP})" ]]; then
+ usermod -aG ${VIDEO_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1
+ fi
# ensure $PROGRAMDATA exists
if [[ ! -d $PROGRAMDATA ]]; then
mkdir $PROGRAMDATA
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index f7b7e3025..e02087a52 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 666937e5c..6962b6bc1 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 0ad0132cc..96e3ca403 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index 4f7ac2099..f1c536399 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index af439e6eb..eaea305d1 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 416d88360..08de71537 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -139,6 +139,9 @@ getent group jellyfin >/dev/null || groupadd -r jellyfin
getent passwd jellyfin >/dev/null || \
useradd -r -g jellyfin -d %{_sharedstatedir}/jellyfin -s /sbin/nologin \
-c "Jellyfin default user" jellyfin
+# Add jellyfin to the render and video groups for hwa.
+[ ! -z "$(getent group render)" ] && usermod -aG render jellyfin >/dev/null 2>&1
+[ ! -z "$(getent group video)" ] && usermod -aG video jellyfin >/dev/null 2>&1
exit 0
%post server
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
index 6cc814ef4..51df09a21 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
@@ -18,8 +18,8 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Moq" Version="4.18.3" />
- <PackageReference Include="SharpFuzz" Version="2.0.0" />
+ <PackageReference Include="Moq" Version="4.18.4" />
+ <PackageReference Include="SharpFuzz" Version="2.0.1" />
</ItemGroup>
</Project>
diff --git a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
index 5e7d14b11..226ab60da 100644
--- a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
+++ b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="SharpFuzz" Version="2.0.0" />
+ <PackageReference Include="SharpFuzz" Version="2.0.1" />
</ItemGroup>
</Project>
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 71385cee2..b611caa11 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -93,6 +93,8 @@
<Rule Id="CA1845" Action="Error" />
<!-- error on CA1849: Call async methods when in an async method -->
<Rule Id="CA1849" Action="Error" />
+ <!-- error on CA1851: Possible multiple enumerations of IEnumerable collection -->
+ <Rule Id="CA1851" Action="Error" />
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
<Rule Id="CA2016" Action="Error" />
@@ -138,6 +140,10 @@
<Rule Id="CA2253" Action="Info" />
<!-- disable warning CA5394: Do not use insecure randomness -->
<Rule Id="CA5394" Action="Info" />
+ <!-- error on CA3003: Review code for file path injection vulnerabilities -->
+ <Rule Id="CA3003" Action="Info" />
+ <!-- error on CA3006: Review code for process command injection vulnerabilities -->
+ <Rule Id="CA3006" Action="Info" />
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index dac3d0a61..a62ebf78c 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
- <Compile Include="..\SharedVersion.cs" />
+ <Compile Include="..\..\SharedVersion.cs" />
</ItemGroup>
<ItemGroup>
@@ -24,14 +24,14 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
new file mode 100644
index 000000000..e2e90be47
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
@@ -0,0 +1,35 @@
+using System;
+using MediaBrowser.Model.Drawing;
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Static helper class used to draw percentage-played indicators on images.
+/// </summary>
+public static class PercentPlayedDrawer
+{
+ private const int IndicatorHeight = 8;
+
+ /// <summary>
+ /// Draw a percentage played indicator on a canvas.
+ /// </summary>
+ /// <param name="canvas">The canvas to draw the indicator on.</param>
+ /// <param name="imageSize">The size of the image being drawn on.</param>
+ /// <param name="percent">The percentage played to display with the indicator.</param>
+ public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
+ {
+ using var paint = new SKPaint();
+ var endX = imageSize.Width - 1;
+ var endY = imageSize.Height - 1;
+
+ paint.Color = SKColor.Parse("#99000000");
+ paint.Style = SKPaintStyle.Fill;
+ canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
+
+ double foregroundWidth = (endX * percent) / 100;
+
+ paint.Color = SKColor.Parse("#FF00A4DC");
+ canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
+ }
+}
diff --git a/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
new file mode 100644
index 000000000..5bb42fb99
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
@@ -0,0 +1,47 @@
+using MediaBrowser.Model.Drawing;
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Static helper class for drawing 'played' indicators.
+/// </summary>
+public static class PlayedIndicatorDrawer
+{
+ private const int OffsetFromTopRightCorner = 38;
+
+ /// <summary>
+ /// Draw a 'played' indicator in the top right corner of a canvas.
+ /// </summary>
+ /// <param name="canvas">The canvas to draw the indicator on.</param>
+ /// <param name="imageSize">
+ /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
+ /// indicator.
+ /// </param>
+ public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize)
+ {
+ var x = imageSize.Width - OffsetFromTopRightCorner;
+
+ using var paint = new SKPaint
+ {
+ Color = SKColor.Parse("#CC00A4DC"),
+ Style = SKPaintStyle.Fill
+ };
+
+ canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
+
+ paint.Color = new SKColor(255, 255, 255, 255);
+ paint.TextSize = 30;
+ paint.IsAntialias = true;
+
+ // or:
+ // var emojiChar = 0x1F680;
+ const string Text = "✔️";
+ var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
+
+ // ask the font manager for a font with that character
+ paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
+
+ canvas.DrawText(Text, (float)x - 12, OffsetFromTopRightCorner + 12, paint);
+ }
+}
diff --git a/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs b/src/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs
index e7db09449..e7db09449 100644
--- a/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs
+++ b/src/Jellyfin.Drawing.Skia/Properties/AssemblyInfo.cs
diff --git a/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs
new file mode 100644
index 000000000..581fa000d
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs
@@ -0,0 +1,44 @@
+using System.Globalization;
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Represents errors that occur during interaction with Skia codecs.
+/// </summary>
+public class SkiaCodecException : SkiaException
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
+ /// </summary>
+ /// <param name="result">The non-successful codec result returned by Skia.</param>
+ public SkiaCodecException(SKCodecResult result)
+ {
+ CodecResult = result;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkiaCodecException" /> class
+ /// with a specified error message.
+ /// </summary>
+ /// <param name="result">The non-successful codec result returned by Skia.</param>
+ /// <param name="message">The message that describes the error.</param>
+ public SkiaCodecException(SKCodecResult result, string message)
+ : base(message)
+ {
+ CodecResult = result;
+ }
+
+ /// <summary>
+ /// Gets the non-successful codec result returned by Skia.
+ /// </summary>
+ public SKCodecResult CodecResult { get; }
+
+ /// <inheritdoc />
+ public override string ToString()
+ => string.Format(
+ CultureInfo.InvariantCulture,
+ "Non-success codec result: {0}\n{1}",
+ CodecResult,
+ base.ToString());
+}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
new file mode 100644
index 000000000..ddb8a98d4
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -0,0 +1,544 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using BlurHashSharp.SkiaSharp;
+using Jellyfin.Extensions;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Drawing;
+using Microsoft.Extensions.Logging;
+using SkiaSharp;
+using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
+/// </summary>
+public class SkiaEncoder : IImageEncoder
+{
+ private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
+
+ private readonly ILogger<SkiaEncoder> _logger;
+ private readonly IApplicationPaths _appPaths;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
+ /// </summary>
+ /// <param name="logger">The application logger.</param>
+ /// <param name="appPaths">The application paths.</param>
+ public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
+ {
+ _logger = logger;
+ _appPaths = appPaths;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Skia";
+
+ /// <inheritdoc/>
+ public bool SupportsImageCollageCreation => true;
+
+ /// <inheritdoc/>
+ public bool SupportsImageEncoding => true;
+
+ /// <inheritdoc/>
+ public IReadOnlyCollection<string> SupportedInputFormats =>
+ new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "jpeg",
+ "jpg",
+ "png",
+ "dng",
+ "webp",
+ "gif",
+ "bmp",
+ "ico",
+ "astc",
+ "ktx",
+ "pkm",
+ "wbmp",
+ // TODO: check if these are supported on multiple platforms
+ // https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
+ // working on windows at least
+ "cr2",
+ "nef",
+ "arw"
+ };
+
+ /// <inheritdoc/>
+ public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
+ => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
+
+ /// <summary>
+ /// Check if the native lib is available.
+ /// </summary>
+ /// <returns>True if the native lib is available, otherwise false.</returns>
+ public static bool IsNativeLibAvailable()
+ {
+ try
+ {
+ // test an operation that requires the native library
+ SKPMColor.PreMultiply(SKColors.Black);
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
+ /// </summary>
+ /// <param name="selectedFormat">The format to convert.</param>
+ /// <returns>The converted format.</returns>
+ public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
+ {
+ return selectedFormat switch
+ {
+ ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
+ ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
+ ImageFormat.Gif => SKEncodedImageFormat.Gif,
+ ImageFormat.Webp => SKEncodedImageFormat.Webp,
+ _ => SKEncodedImageFormat.Png
+ };
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="FileNotFoundException">The path is not valid.</exception>
+ public ImageDimensions GetImageSize(string path)
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException("File not found", path);
+ }
+
+ var extension = Path.GetExtension(path.AsSpan());
+ if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
+ {
+ var svg = new SKSvg();
+ svg.Load(path);
+ return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
+ }
+
+ using var codec = SKCodec.Create(path, out SKCodecResult result);
+ switch (result)
+ {
+ case SKCodecResult.Success:
+ var info = codec.Info;
+ return new ImageDimensions(info.Width, info.Height);
+ case SKCodecResult.Unimplemented:
+ _logger.LogDebug("Image format not supported: {FilePath}", path);
+ return new ImageDimensions(0, 0);
+ default:
+ _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
+ return new ImageDimensions(0, 0);
+ }
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">The path is null.</exception>
+ /// <exception cref="FileNotFoundException">The path is not valid.</exception>
+ /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
+ public string GetImageBlurHash(int xComp, int yComp, string path)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
+ if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
+ return string.Empty;
+ }
+
+ // Any larger than 128x128 is too slow and there's no visually discernible difference
+ return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
+ }
+
+ private bool RequiresSpecialCharacterHack(string path)
+ {
+ for (int i = 0; i < path.Length; i++)
+ {
+ if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
+ {
+ return true;
+ }
+ }
+
+ return path.HasDiacritics();
+ }
+
+ private string NormalizePath(string path)
+ {
+ if (!RequiresSpecialCharacterHack(path))
+ {
+ return path;
+ }
+
+ var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
+ var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
+ Directory.CreateDirectory(directory);
+ File.Copy(path, tempPath, true);
+
+ return tempPath;
+ }
+
+ private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
+ {
+ if (!orientation.HasValue)
+ {
+ return SKEncodedOrigin.TopLeft;
+ }
+
+ return orientation.Value switch
+ {
+ ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
+ ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
+ ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
+ ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
+ ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
+ ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
+ ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
+ _ => SKEncodedOrigin.TopLeft
+ };
+ }
+
+ /// <summary>
+ /// Decode an image.
+ /// </summary>
+ /// <param name="path">The filepath of the image to decode.</param>
+ /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
+ /// <param name="orientation">The orientation of the image.</param>
+ /// <param name="origin">The detected origin of the image.</param>
+ /// <returns>The resulting bitmap of the image.</returns>
+ internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException("File not found", path);
+ }
+
+ var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
+
+ if (requiresTransparencyHack || forceCleanBitmap)
+ {
+ using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
+ if (res != SKCodecResult.Success)
+ {
+ origin = GetSKEncodedOrigin(orientation);
+ return null;
+ }
+
+ // create the bitmap
+ var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
+
+ // decode
+ _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
+
+ origin = codec.EncodedOrigin;
+
+ return bitmap;
+ }
+
+ var resultBitmap = SKBitmap.Decode(NormalizePath(path));
+
+ if (resultBitmap is null)
+ {
+ return Decode(path, true, orientation, out origin);
+ }
+
+ // If we have to resize these they often end up distorted
+ if (resultBitmap.ColorType == SKColorType.Gray8)
+ {
+ using (resultBitmap)
+ {
+ return Decode(path, true, orientation, out origin);
+ }
+ }
+
+ origin = SKEncodedOrigin.TopLeft;
+ return resultBitmap;
+ }
+
+ private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
+ {
+ if (autoOrient)
+ {
+ var bitmap = Decode(path, true, orientation, out var origin);
+
+ if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
+ {
+ using (bitmap)
+ {
+ return OrientImage(bitmap, origin);
+ }
+ }
+
+ return bitmap;
+ }
+
+ return Decode(path, false, orientation, out _);
+ }
+
+ private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
+ {
+ var needsFlip = origin == SKEncodedOrigin.LeftBottom
+ || origin == SKEncodedOrigin.LeftTop
+ || origin == SKEncodedOrigin.RightBottom
+ || origin == SKEncodedOrigin.RightTop;
+ var rotated = needsFlip
+ ? new SKBitmap(bitmap.Height, bitmap.Width)
+ : new SKBitmap(bitmap.Width, bitmap.Height);
+ using var surface = new SKCanvas(rotated);
+ var midX = (float)rotated.Width / 2;
+ var midY = (float)rotated.Height / 2;
+
+ switch (origin)
+ {
+ case SKEncodedOrigin.TopRight:
+ surface.Scale(-1, 1, midX, midY);
+ break;
+ case SKEncodedOrigin.BottomRight:
+ surface.RotateDegrees(180, midX, midY);
+ break;
+ case SKEncodedOrigin.BottomLeft:
+ surface.Scale(1, -1, midX, midY);
+ break;
+ case SKEncodedOrigin.LeftTop:
+ surface.Translate(0, -rotated.Height);
+ surface.Scale(1, -1, midX, midY);
+ surface.RotateDegrees(-90);
+ break;
+ case SKEncodedOrigin.RightTop:
+ surface.Translate(rotated.Width, 0);
+ surface.RotateDegrees(90);
+ break;
+ case SKEncodedOrigin.RightBottom:
+ surface.Translate(rotated.Width, 0);
+ surface.Scale(1, -1, midX, midY);
+ surface.RotateDegrees(90);
+ break;
+ case SKEncodedOrigin.LeftBottom:
+ surface.Translate(0, rotated.Height);
+ surface.RotateDegrees(-90);
+ break;
+ }
+
+ surface.DrawBitmap(bitmap, 0, 0);
+ return rotated;
+ }
+
+ /// <summary>
+ /// Resizes an image on the CPU, by utilizing a surface and canvas.
+ ///
+ /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
+ /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html).
+ /// </summary>
+ /// <param name="source">The source bitmap.</param>
+ /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
+ /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
+ /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
+ /// <returns>The resized image.</returns>
+ internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
+ {
+ using var surface = SKSurface.Create(targetInfo);
+ using var canvas = surface.Canvas;
+ using var paint = new SKPaint
+ {
+ FilterQuality = SKFilterQuality.High,
+ IsAntialias = isAntialias,
+ IsDither = isDither
+ };
+
+ var kernel = new float[9]
+ {
+ 0, -.1f, 0,
+ -.1f, 1.4f, -.1f,
+ 0, -.1f, 0,
+ };
+
+ var kernelSize = new SKSizeI(3, 3);
+ var kernelOffset = new SKPointI(1, 1);
+
+ paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
+ kernelSize,
+ kernel,
+ 1f,
+ 0f,
+ kernelOffset,
+ SKShaderTileMode.Clamp,
+ true);
+
+ canvas.DrawBitmap(
+ source,
+ SKRect.Create(0, 0, source.Width, source.Height),
+ SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
+ paint);
+
+ return surface.Snapshot();
+ }
+
+ /// <inheritdoc/>
+ public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(inputPath);
+ ArgumentException.ThrowIfNullOrEmpty(outputPath);
+
+ var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
+ if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
+ return inputPath;
+ }
+
+ var skiaOutputFormat = GetImageFormat(outputFormat);
+
+ var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
+ var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
+ var blur = options.Blur ?? 0;
+ var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
+
+ using var bitmap = GetBitmap(inputPath, autoOrient, orientation);
+ if (bitmap is null)
+ {
+ throw new InvalidDataException($"Skia unable to read image {inputPath}");
+ }
+
+ var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
+
+ if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
+ {
+ // Just spit out the original file if all the options are default
+ return inputPath;
+ }
+
+ var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
+
+ var width = newImageSize.Width;
+ var height = newImageSize.Height;
+
+ // scale image (the FromImage creates a copy)
+ var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
+ using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
+
+ // If all we're doing is resizing then we can stop now
+ if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
+ {
+ var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ Directory.CreateDirectory(outputDirectory);
+ using var outputStream = new SKFileWStream(outputPath);
+ using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
+ resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
+ return outputPath;
+ }
+
+ // create bitmap to use for canvas drawing used to draw into bitmap
+ using var saveBitmap = new SKBitmap(width, height);
+ using var canvas = new SKCanvas(saveBitmap);
+ // set background color if present
+ if (hasBackgroundColor)
+ {
+ canvas.Clear(SKColor.Parse(options.BackgroundColor));
+ }
+
+ // Add blur if option is present
+ if (blur > 0)
+ {
+ // create image from resized bitmap to apply blur
+ using var paint = new SKPaint();
+ using var filter = SKImageFilter.CreateBlur(blur, blur);
+ paint.ImageFilter = filter;
+ canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
+ }
+ else
+ {
+ // draw resized bitmap onto canvas
+ canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
+ }
+
+ // If foreground layer present then draw
+ if (hasForegroundColor)
+ {
+ if (!double.TryParse(options.ForegroundLayer, out double opacity))
+ {
+ opacity = .4;
+ }
+
+ canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
+ }
+
+ if (hasIndicator)
+ {
+ DrawIndicator(canvas, width, height, options);
+ }
+
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ Directory.CreateDirectory(directory);
+ using (var outputStream = new SKFileWStream(outputPath))
+ {
+ using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
+ {
+ pixmap.Encode(outputStream, skiaOutputFormat, quality);
+ }
+ }
+
+ return outputPath;
+ }
+
+ /// <inheritdoc/>
+ public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
+ {
+ double ratio = (double)options.Width / options.Height;
+
+ if (ratio >= 1.4)
+ {
+ new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName);
+ }
+ else if (ratio >= .9)
+ {
+ new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+ }
+ else
+ {
+ // TODO: Create Poster collage capability
+ new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+ }
+ }
+
+ /// <inheritdoc />
+ public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
+ {
+ var splashBuilder = new SplashscreenBuilder(this);
+ var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
+ splashBuilder.GenerateSplash(posters, backdrops, outputPath);
+ }
+
+ private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
+ {
+ try
+ {
+ var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
+
+ if (options.AddPlayedIndicator)
+ {
+ PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize);
+ }
+ else if (options.UnplayedCount.HasValue)
+ {
+ UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
+ }
+
+ if (options.PercentPlayed > 0)
+ {
+ PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error drawing indicator overlay");
+ }
+ }
+}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaException.cs b/src/Jellyfin.Drawing.Skia/SkiaException.cs
new file mode 100644
index 000000000..d0e69d42c
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/SkiaException.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Represents errors that occur during interaction with Skia.
+/// </summary>
+public class SkiaException : Exception
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkiaException"/> class.
+ /// </summary>
+ public SkiaException()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ public SkiaException(string message) : base(message)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message and a
+ /// reference to the inner exception that is the cause of this exception.
+ /// </summary>
+ /// <param name="message">The error message that explains the reason for the exception.</param>
+ /// <param name="innerException">
+ /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
+ /// no inner exception is specified.
+ /// </param>
+ public SkiaException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
new file mode 100644
index 000000000..00d224da9
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Class containing helper methods for working with SkiaSharp.
+/// </summary>
+public static class SkiaHelper
+{
+ /// <summary>
+ /// Gets the next valid image as a bitmap.
+ /// </summary>
+ /// <param name="skiaEncoder">The current skia encoder.</param>
+ /// <param name="paths">The list of image paths.</param>
+ /// <param name="currentIndex">The current checked index.</param>
+ /// <param name="newIndex">The new index.</param>
+ /// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns>
+ public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)
+ {
+ var imagesTested = new Dictionary<int, int>();
+ SKBitmap? bitmap = null;
+
+ while (imagesTested.Count < paths.Count)
+ {
+ if (currentIndex >= paths.Count)
+ {
+ currentIndex = 0;
+ }
+
+ bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
+
+ imagesTested[currentIndex] = 0;
+
+ currentIndex++;
+
+ if (bitmap is not null)
+ {
+ break;
+ }
+ }
+
+ newIndex = currentIndex;
+ return bitmap;
+ }
+}
diff --git a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
new file mode 100644
index 000000000..990556623
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Used to build the splashscreen.
+/// </summary>
+public class SplashscreenBuilder
+{
+ private const int FinalWidth = 1920;
+ private const int FinalHeight = 1080;
+ // generated collage resolution should be higher than the final resolution
+ private const int WallWidth = FinalWidth * 3;
+ private const int WallHeight = FinalHeight * 2;
+ private const int Rows = 6;
+ private const int Spacing = 20;
+
+ private readonly SkiaEncoder _skiaEncoder;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class.
+ /// </summary>
+ /// <param name="skiaEncoder">The SkiaEncoder.</param>
+ public SplashscreenBuilder(SkiaEncoder skiaEncoder)
+ {
+ _skiaEncoder = skiaEncoder;
+ }
+
+ /// <summary>
+ /// Generate a splashscreen.
+ /// </summary>
+ /// <param name="posters">The poster paths.</param>
+ /// <param name="backdrops">The landscape paths.</param>
+ /// <param name="outputPath">The output path.</param>
+ public void GenerateSplash(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops, string outputPath)
+ {
+ using var wall = GenerateCollage(posters, backdrops);
+ using var transformed = Transform3D(wall);
+
+ using var outputStream = new SKFileWStream(outputPath);
+ using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels());
+ pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90);
+ }
+
+ /// <summary>
+ /// Generates a collage of posters and landscape pictures.
+ /// </summary>
+ /// <param name="posters">The poster paths.</param>
+ /// <param name="backdrops">The landscape paths.</param>
+ /// <returns>The created collage as a bitmap.</returns>
+ private SKBitmap GenerateCollage(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
+ {
+ var posterIndex = 0;
+ var backdropIndex = 0;
+
+ var bitmap = new SKBitmap(WallWidth, WallHeight);
+ using var canvas = new SKCanvas(bitmap);
+ canvas.Clear(SKColors.Black);
+
+ int posterHeight = WallHeight / 6;
+
+ for (int i = 0; i < Rows; i++)
+ {
+ int imageCounter = Random.Shared.Next(0, 5);
+ int currentWidthPos = i * 75;
+ int currentHeight = i * (posterHeight + Spacing);
+
+ while (currentWidthPos < WallWidth)
+ {
+ SKBitmap? currentImage;
+
+ switch (imageCounter)
+ {
+ case 0:
+ case 2:
+ case 3:
+ currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
+ posterIndex = newPosterIndex;
+ break;
+ default:
+ currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
+ backdropIndex = newBackdropIndex;
+ break;
+ }
+
+ if (currentImage is null)
+ {
+ throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!");
+ }
+
+ // resize to the same aspect as the original
+ var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
+ using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
+ currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
+
+ // draw on canvas
+ canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
+
+ currentWidthPos += imageWidth + Spacing;
+
+ currentImage.Dispose();
+
+ if (imageCounter >= 4)
+ {
+ imageCounter = 0;
+ }
+ else
+ {
+ imageCounter++;
+ }
+ }
+ }
+
+ return bitmap;
+ }
+
+ /// <summary>
+ /// Transform the collage in 3D space.
+ /// </summary>
+ /// <param name="input">The bitmap to transform.</param>
+ /// <returns>The transformed image.</returns>
+ private SKBitmap Transform3D(SKBitmap input)
+ {
+ var bitmap = new SKBitmap(FinalWidth, FinalHeight);
+ using var canvas = new SKCanvas(bitmap);
+ canvas.Clear(SKColors.Black);
+ var matrix = new SKMatrix
+ {
+ ScaleX = 0.324108899f,
+ ScaleY = 0.563934922f,
+ SkewX = -0.244337708f,
+ SkewY = 0.0377609022f,
+ TransX = 42.0407715f,
+ TransY = -198.104706f,
+ Persp0 = -9.08959337E-05f,
+ Persp1 = 6.85242048E-05f,
+ Persp2 = 0.988209724f
+ };
+
+ canvas.SetMatrix(matrix);
+ canvas.DrawBitmap(input, 0, 0);
+
+ return bitmap;
+ }
+}
diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
new file mode 100644
index 000000000..eee24c423
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Used to build collages of multiple images arranged in vertical strips.
+/// </summary>
+public class StripCollageBuilder
+{
+ private readonly SkiaEncoder _skiaEncoder;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StripCollageBuilder"/> class.
+ /// </summary>
+ /// <param name="skiaEncoder">The encoder to use for building collages.</param>
+ public StripCollageBuilder(SkiaEncoder skiaEncoder)
+ {
+ _skiaEncoder = skiaEncoder;
+ }
+
+ /// <summary>
+ /// Check which format an image has been encoded with using its filename extension.
+ /// </summary>
+ /// <param name="outputPath">The path to the image to get the format for.</param>
+ /// <returns>The image format.</returns>
+ public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
+ {
+ ArgumentNullException.ThrowIfNull(outputPath);
+
+ var ext = Path.GetExtension(outputPath);
+
+ if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return SKEncodedImageFormat.Jpeg;
+ }
+
+ if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
+ {
+ return SKEncodedImageFormat.Webp;
+ }
+
+ if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
+ {
+ return SKEncodedImageFormat.Gif;
+ }
+
+ if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
+ {
+ return SKEncodedImageFormat.Bmp;
+ }
+
+ // default to png
+ return SKEncodedImageFormat.Png;
+ }
+
+ /// <summary>
+ /// Create a square collage.
+ /// </summary>
+ /// <param name="paths">The paths of the images to use in the collage.</param>
+ /// <param name="outputPath">The path at which to place the resulting collage image.</param>
+ /// <param name="width">The desired width of the collage.</param>
+ /// <param name="height">The desired height of the collage.</param>
+ public void BuildSquareCollage(IReadOnlyList<string> paths, string outputPath, int width, int height)
+ {
+ using var bitmap = BuildSquareCollageBitmap(paths, width, height);
+ using var outputStream = new SKFileWStream(outputPath);
+ using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+ pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+ }
+
+ /// <summary>
+ /// Create a thumb collage.
+ /// </summary>
+ /// <param name="paths">The paths of the images to use in the collage.</param>
+ /// <param name="outputPath">The path at which to place the resulting image.</param>
+ /// <param name="width">The desired width of the collage.</param>
+ /// <param name="height">The desired height of the collage.</param>
+ /// <param name="libraryName">The name of the library to draw on the collage.</param>
+ public void BuildThumbCollage(IReadOnlyList<string> paths, string outputPath, int width, int height, string? libraryName)
+ {
+ using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
+ using var outputStream = new SKFileWStream(outputPath);
+ using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+ pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+ }
+
+ private SKBitmap BuildThumbCollageBitmap(IReadOnlyList<string> paths, int width, int height, string? libraryName)
+ {
+ var bitmap = new SKBitmap(width, height);
+
+ using var canvas = new SKCanvas(bitmap);
+ canvas.Clear(SKColors.Black);
+
+ using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _);
+ if (backdrop is null)
+ {
+ return bitmap;
+ }
+
+ // resize to the same aspect as the original
+ var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
+ using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
+ // draw the backdrop
+ canvas.DrawImage(residedBackdrop, 0, 0);
+
+ // draw shadow rectangle
+ using var paintColor = new SKPaint
+ {
+ Color = SKColors.Black.WithAlpha(0x78),
+ Style = SKPaintStyle.Fill
+ };
+ canvas.DrawRect(0, 0, width, height, paintColor);
+
+ var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
+
+ // use the system fallback to find a typeface for the given CJK character
+ var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
+ var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
+ if (!string.IsNullOrEmpty(filteredName))
+ {
+ typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
+ }
+
+ // draw library name
+ using var textPaint = new SKPaint
+ {
+ Color = SKColors.White,
+ Style = SKPaintStyle.Fill,
+ TextSize = 112,
+ TextAlign = SKTextAlign.Center,
+ Typeface = typeFace,
+ IsAntialias = true
+ };
+
+ // scale down text to 90% of the width if text is larger than 95% of the width
+ var textWidth = textPaint.MeasureText(libraryName);
+ if (textWidth > width * 0.95)
+ {
+ textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
+ }
+
+ canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+
+ return bitmap;
+ }
+
+ private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height)
+ {
+ var bitmap = new SKBitmap(width, height);
+ var imageIndex = 0;
+ var cellWidth = width / 2;
+ var cellHeight = height / 2;
+
+ using var canvas = new SKCanvas(bitmap);
+ for (var x = 0; x < 2; x++)
+ {
+ for (var y = 0; y < 2; y++)
+ {
+ using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex);
+ imageIndex = newIndex;
+
+ if (currentBitmap is null)
+ {
+ continue;
+ }
+
+ // Scale image. The FromBitmap creates a copy
+ var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
+ using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
+
+ // draw this image into the strip at the next position
+ var xPos = x * cellWidth;
+ var yPos = y * cellHeight;
+ canvas.DrawBitmap(resizedBitmap, xPos, yPos);
+ }
+ }
+
+ return bitmap;
+ }
+}
diff --git a/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
new file mode 100644
index 000000000..456b84b8c
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
@@ -0,0 +1,63 @@
+using System.Globalization;
+using MediaBrowser.Model.Drawing;
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Static helper class for drawing unplayed count indicators.
+/// </summary>
+public static class UnplayedCountIndicator
+{
+ /// <summary>
+ /// The x-offset used when drawing an unplayed count indicator.
+ /// </summary>
+ private const int OffsetFromTopRightCorner = 38;
+
+ /// <summary>
+ /// Draw an unplayed count indicator in the top right corner of a canvas.
+ /// </summary>
+ /// <param name="canvas">The canvas to draw the indicator on.</param>
+ /// <param name="imageSize">
+ /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
+ /// indicator.
+ /// </param>
+ /// <param name="count">The number to draw in the indicator.</param>
+ public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count)
+ {
+ var x = imageSize.Width - OffsetFromTopRightCorner;
+ var text = count.ToString(CultureInfo.InvariantCulture);
+
+ using var paint = new SKPaint
+ {
+ Color = SKColor.Parse("#CC00A4DC"),
+ Style = SKPaintStyle.Fill
+ };
+
+ canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
+
+ paint.Color = new SKColor(255, 255, 255, 255);
+ paint.TextSize = 24;
+ paint.IsAntialias = true;
+
+ var y = OffsetFromTopRightCorner + 9;
+
+ if (text.Length == 1)
+ {
+ x -= 7;
+ }
+
+ if (text.Length == 2)
+ {
+ x -= 13;
+ }
+ else if (text.Length >= 3)
+ {
+ x -= 15;
+ y -= 2;
+ paint.TextSize = 18;
+ }
+
+ canvas.DrawText(text, x, y, paint);
+ }
+}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
new file mode 100644
index 000000000..353a27b25
--- /dev/null
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -0,0 +1,595 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net.Mime;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using Microsoft.Extensions.Logging;
+using Photo = MediaBrowser.Controller.Entities.Photo;
+
+namespace Jellyfin.Drawing;
+
+/// <summary>
+/// Class ImageProcessor.
+/// </summary>
+public sealed class ImageProcessor : IImageProcessor, IDisposable
+{
+ // Increment this when there's a change requiring caches to be invalidated
+ private const char Version = '3';
+
+ private static readonly HashSet<string> _transparentImageTypes
+ = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
+
+ private readonly ILogger<ImageProcessor> _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerApplicationPaths _appPaths;
+ private readonly IImageEncoder _imageEncoder;
+ private readonly IMediaEncoder _mediaEncoder;
+
+ private readonly SemaphoreSlim _parallelEncodingLimit;
+
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ImageProcessor"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="appPaths">The server application paths.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ /// <param name="imageEncoder">The image encoder.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="config">The configuration.</param>
+ public ImageProcessor(
+ ILogger<ImageProcessor> logger,
+ IServerApplicationPaths appPaths,
+ IFileSystem fileSystem,
+ IImageEncoder imageEncoder,
+ IMediaEncoder mediaEncoder,
+ IServerConfigurationManager config)
+ {
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _imageEncoder = imageEncoder;
+ _mediaEncoder = mediaEncoder;
+ _appPaths = appPaths;
+
+ var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
+ if (semaphoreCount < 1)
+ {
+ semaphoreCount = 2 * Environment.ProcessorCount;
+ }
+
+ _parallelEncodingLimit = new(semaphoreCount, semaphoreCount);
+ }
+
+ private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
+
+ /// <inheritdoc />
+ public IReadOnlyCollection<string> SupportedInputFormats =>
+ new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "tiff",
+ "tif",
+ "jpeg",
+ "jpg",
+ "png",
+ "aiff",
+ "cr2",
+ "crw",
+ "nef",
+ "orf",
+ "pef",
+ "arw",
+ "webp",
+ "gif",
+ "bmp",
+ "erf",
+ "raf",
+ "rw2",
+ "nrw",
+ "dng",
+ "ico",
+ "astc",
+ "ktx",
+ "pkm",
+ "wbmp"
+ };
+
+ /// <inheritdoc />
+ public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
+
+ /// <inheritdoc />
+ public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
+ {
+ var file = await ProcessImage(options).ConfigureAwait(false);
+ using (var fileStream = AsyncFile.OpenRead(file.Path))
+ {
+ await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
+ }
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
+ => _imageEncoder.SupportedOutputFormats;
+
+ /// <inheritdoc />
+ public bool SupportsTransparency(string path)
+ => _transparentImageTypes.Contains(Path.GetExtension(path));
+
+ /// <inheritdoc />
+ public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
+ {
+ ItemImageInfo originalImage = options.Image;
+ BaseItem item = options.Item;
+
+ string originalImagePath = originalImage.Path;
+ DateTime dateModified = originalImage.DateModified;
+ ImageDimensions? originalImageSize = null;
+ if (originalImage.Width > 0 && originalImage.Height > 0)
+ {
+ originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
+ }
+
+ var mimeType = MimeTypes.GetMimeType(originalImagePath);
+ if (!_imageEncoder.SupportsImageEncoding)
+ {
+ return (originalImagePath, mimeType, dateModified);
+ }
+
+ var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
+ originalImagePath = supportedImageInfo.Path;
+
+ // Original file doesn't exist, or original file is gif.
+ if (!File.Exists(originalImagePath) || string.Equals(mimeType, MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase))
+ {
+ return (originalImagePath, mimeType, dateModified);
+ }
+
+ dateModified = supportedImageInfo.DateModified;
+ bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
+
+ bool autoOrient = false;
+ ImageOrientation? orientation = null;
+ if (item is Photo photo)
+ {
+ if (photo.Orientation.HasValue)
+ {
+ if (photo.Orientation.Value != ImageOrientation.TopLeft)
+ {
+ autoOrient = true;
+ orientation = photo.Orientation;
+ }
+ }
+ else
+ {
+ // Orientation unknown, so do it
+ autoOrient = true;
+ orientation = photo.Orientation;
+ }
+ }
+
+ if (options.HasDefaultOptions(originalImagePath, originalImageSize) && (!autoOrient || !options.RequiresAutoOrientation))
+ {
+ // Just spit out the original file if all the options are default
+ return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+ }
+
+ int quality = options.Quality;
+
+ ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
+ string cacheFilePath = GetCacheFilePath(
+ originalImagePath,
+ options.Width,
+ options.Height,
+ options.MaxWidth,
+ options.MaxHeight,
+ options.FillWidth,
+ options.FillHeight,
+ quality,
+ dateModified,
+ outputFormat,
+ options.AddPlayedIndicator,
+ options.PercentPlayed,
+ options.UnplayedCount,
+ options.Blur,
+ options.BackgroundColor,
+ options.ForegroundLayer);
+
+ try
+ {
+ if (!File.Exists(cacheFilePath))
+ {
+ // Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
+ await _parallelEncodingLimit.WaitAsync().ConfigureAwait(false);
+
+ string resultPath;
+ try
+ {
+ resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
+ }
+ finally
+ {
+ _parallelEncodingLimit.Release();
+ }
+
+ if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
+ {
+ return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+ }
+ }
+
+ return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
+ }
+ catch (Exception ex)
+ {
+ // If it fails for whatever reason, return the original image
+ _logger.LogError(ex, "Error encoding image");
+ return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+ }
+ }
+
+ private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
+ {
+ var serverFormats = GetSupportedImageOutputFormats();
+
+ // Client doesn't care about format, so start with webp if supported
+ if (serverFormats.Contains(ImageFormat.Webp) && clientSupportedFormats.Contains(ImageFormat.Webp))
+ {
+ return ImageFormat.Webp;
+ }
+
+ // If transparency is needed and webp isn't supported, than png is the only option
+ if (requiresTransparency && clientSupportedFormats.Contains(ImageFormat.Png))
+ {
+ return ImageFormat.Png;
+ }
+
+ foreach (var format in clientSupportedFormats)
+ {
+ if (serverFormats.Contains(format))
+ {
+ return format;
+ }
+ }
+
+ // We should never actually get here
+ return ImageFormat.Jpg;
+ }
+
+ private string GetMimeType(ImageFormat format, string path)
+ => format switch
+ {
+ ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
+ ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
+ ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
+ ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
+ ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
+ _ => MimeTypes.GetMimeType(path)
+ };
+
+ /// <summary>
+ /// Gets the cache file path based on a set of parameters.
+ /// </summary>
+ private string GetCacheFilePath(
+ string originalPath,
+ int? width,
+ int? height,
+ int? maxWidth,
+ int? maxHeight,
+ int? fillWidth,
+ int? fillHeight,
+ int quality,
+ DateTime dateModified,
+ ImageFormat format,
+ bool addPlayedIndicator,
+ double percentPlayed,
+ int? unwatchedCount,
+ int? blur,
+ string backgroundColor,
+ string foregroundLayer)
+ {
+ var filename = new StringBuilder(256);
+ filename.Append(originalPath);
+
+ filename.Append(",quality=");
+ filename.Append(quality);
+
+ filename.Append(",datemodified=");
+ filename.Append(dateModified.Ticks);
+
+ filename.Append(",f=");
+ filename.Append(format);
+
+ if (width.HasValue)
+ {
+ filename.Append(",width=");
+ filename.Append(width.Value);
+ }
+
+ if (height.HasValue)
+ {
+ filename.Append(",height=");
+ filename.Append(height.Value);
+ }
+
+ if (maxWidth.HasValue)
+ {
+ filename.Append(",maxwidth=");
+ filename.Append(maxWidth.Value);
+ }
+
+ if (maxHeight.HasValue)
+ {
+ filename.Append(",maxheight=");
+ filename.Append(maxHeight.Value);
+ }
+
+ if (fillWidth.HasValue)
+ {
+ filename.Append(",fillwidth=");
+ filename.Append(fillWidth.Value);
+ }
+
+ if (fillHeight.HasValue)
+ {
+ filename.Append(",fillheight=");
+ filename.Append(fillHeight.Value);
+ }
+
+ if (addPlayedIndicator)
+ {
+ filename.Append(",pl=true");
+ }
+
+ if (percentPlayed > 0)
+ {
+ filename.Append(",p=");
+ filename.Append(percentPlayed);
+ }
+
+ if (unwatchedCount.HasValue)
+ {
+ filename.Append(",p=");
+ filename.Append(unwatchedCount.Value);
+ }
+
+ if (blur.HasValue)
+ {
+ filename.Append(",blur=");
+ filename.Append(blur.Value);
+ }
+
+ if (!string.IsNullOrEmpty(backgroundColor))
+ {
+ filename.Append(",b=");
+ filename.Append(backgroundColor);
+ }
+
+ if (!string.IsNullOrEmpty(foregroundLayer))
+ {
+ filename.Append(",fl=");
+ filename.Append(foregroundLayer);
+ }
+
+ filename.Append(",v=");
+ filename.Append(Version);
+
+ return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
+ }
+
+ /// <inheritdoc />
+ public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info)
+ {
+ int width = info.Width;
+ int height = info.Height;
+
+ if (height > 0 && width > 0)
+ {
+ return new ImageDimensions(width, height);
+ }
+
+ string path = info.Path;
+ _logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
+
+ ImageDimensions size = GetImageDimensions(path);
+ info.Width = size.Width;
+ info.Height = size.Height;
+
+ return size;
+ }
+
+ /// <inheritdoc />
+ public ImageDimensions GetImageDimensions(string path)
+ => _imageEncoder.GetImageSize(path);
+
+ /// <inheritdoc />
+ public string GetImageBlurHash(string path)
+ {
+ var size = GetImageDimensions(path);
+ return GetImageBlurHash(path, size);
+ }
+
+ /// <inheritdoc />
+ public string GetImageBlurHash(string path, ImageDimensions imageDimensions)
+ {
+ if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0)
+ {
+ return string.Empty;
+ }
+
+ // We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
+ // One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
+ // See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
+ float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
+ float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
+
+ int xComp = Math.Min((int)xCompF + 1, 9);
+ int yComp = Math.Min((int)yCompF + 1, 9);
+
+ return _imageEncoder.GetImageBlurHash(xComp, yComp, path);
+ }
+
+ /// <inheritdoc />
+ public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
+ => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+
+ /// <inheritdoc />
+ public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
+ {
+ return GetImageCacheTag(item, new ItemImageInfo
+ {
+ Path = chapter.ImagePath,
+ Type = ImageType.Chapter,
+ DateModified = chapter.ImageDateModified
+ });
+ }
+
+ /// <inheritdoc />
+ public string? GetImageCacheTag(User user)
+ {
+ if (user.ProfileImage is null)
+ {
+ return null;
+ }
+
+ return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
+ .ToString("N", CultureInfo.InvariantCulture);
+ }
+
+ private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
+ {
+ var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
+
+ // These are just jpg files renamed as tbn
+ if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
+ {
+ return Task.FromResult((originalImagePath, dateModified));
+ }
+
+ // TODO _mediaEncoder.ConvertImage is not implemented
+ // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
+ // {
+ // try
+ // {
+ // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ //
+ // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
+ // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
+ //
+ // var file = _fileSystem.GetFileInfo(outputPath);
+ // if (!file.Exists)
+ // {
+ // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
+ // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
+ // }
+ // else
+ // {
+ // dateModified = file.LastWriteTimeUtc;
+ // }
+ //
+ // originalImagePath = outputPath;
+ // }
+ // catch (Exception ex)
+ // {
+ // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
+ // }
+ // }
+
+ return Task.FromResult((originalImagePath, dateModified));
+ }
+
+ /// <summary>
+ /// Gets the cache path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="uniqueName">Name of the unique.</param>
+ /// <param name="fileExtension">The file extension.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="ArgumentNullException">
+ /// path
+ /// or
+ /// uniqueName
+ /// or
+ /// fileExtension.
+ /// </exception>
+ public string GetCachePath(string path, string uniqueName, string fileExtension)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+ ArgumentException.ThrowIfNullOrEmpty(uniqueName);
+ ArgumentException.ThrowIfNullOrEmpty(fileExtension);
+
+ var filename = uniqueName.GetMD5() + fileExtension;
+
+ return GetCachePath(path, filename);
+ }
+
+ /// <summary>
+ /// Gets the cache path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="filename">The filename.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="ArgumentNullException">
+ /// path
+ /// or
+ /// filename.
+ /// </exception>
+ public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename)
+ {
+ if (path.IsEmpty)
+ {
+ throw new ArgumentException("Path can't be empty.", nameof(path));
+ }
+
+ if (filename.IsEmpty)
+ {
+ throw new ArgumentException("Filename can't be empty.", nameof(filename));
+ }
+
+ var prefix = filename.Slice(0, 1);
+
+ return Path.Join(path, prefix, filename);
+ }
+
+ /// <inheritdoc />
+ public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
+ {
+ _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
+
+ _imageEncoder.CreateImageCollage(options, libraryName);
+
+ _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (_imageEncoder is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+
+ _parallelEncodingLimit?.Dispose();
+
+ _disposed = true;
+ }
+}
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index 5bf226408..7aa994503 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -12,18 +12,18 @@
</PropertyGroup>
<ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
<ItemGroup>
- <Compile Include="..\SharedVersion.cs" />
+ <Compile Include="..\..\SharedVersion.cs" />
</ItemGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs
new file mode 100644
index 000000000..171128bed
--- /dev/null
+++ b/src/Jellyfin.Drawing/NullImageEncoder.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Drawing;
+
+namespace Jellyfin.Drawing;
+
+/// <summary>
+/// A fallback implementation of <see cref="IImageEncoder" />.
+/// </summary>
+public class NullImageEncoder : IImageEncoder
+{
+ /// <inheritdoc />
+ public IReadOnlyCollection<string> SupportedInputFormats
+ => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" };
+
+ /// <inheritdoc />
+ public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
+ => new HashSet<ImageFormat>() { ImageFormat.Jpg, ImageFormat.Png };
+
+ /// <inheritdoc />
+ public string Name => "Null Image Encoder";
+
+ /// <inheritdoc />
+ public bool SupportsImageCollageCreation => false;
+
+ /// <inheritdoc />
+ public bool SupportsImageEncoding => false;
+
+ /// <inheritdoc />
+ public ImageDimensions GetImageSize(string path)
+ => throw new NotImplementedException();
+
+ /// <inheritdoc />
+ public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public string GetImageBlurHash(int xComp, int yComp, string path)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Emby.Drawing/Properties/AssemblyInfo.cs b/src/Jellyfin.Drawing/Properties/AssemblyInfo.cs
index 281008e37..3851bf924 100644
--- a/Emby.Drawing/Properties/AssemblyInfo.cs
+++ b/src/Jellyfin.Drawing/Properties/AssemblyInfo.cs
@@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
-[assembly: AssemblyTitle("Emby.Drawing")]
+[assembly: AssemblyTitle("Jellyfin.Drawing")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")]
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index eaf2bc35c..d7c05ea57 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -27,9 +27,14 @@
<Compile Include="../../SharedVersion.cs" />
</ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Diacritics" Version="3.3.14" />
+ </ItemGroup>
+
<!-- Code Analyzers-->
<ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index b19be071b..f30b63945 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -20,23 +20,8 @@ namespace Jellyfin.Extensions
/// <param name="text">The string to act on.</param>
/// <returns>The string without diacritics character.</returns>
public static string RemoveDiacritics(this string text)
- {
- string withDiactritics = _nonConformingUnicode
- .Replace(text, string.Empty)
- .Normalize(NormalizationForm.FormD);
-
- var withoutDiactritics = new StringBuilder();
- foreach (char c in withDiactritics)
- {
- UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(c);
- if (uc != UnicodeCategory.NonSpacingMark)
- {
- withoutDiactritics.Append(c);
- }
- }
-
- return withoutDiactritics.ToString().Normalize(NormalizationForm.FormC);
- }
+ => Diacritics.Extensions.StringExtensions.RemoveDiacritics(
+ _nonConformingUnicode.Replace(text, string.Empty));
/// <summary>
/// Checks whether or not the specified string has diacritics in it.
@@ -44,9 +29,8 @@ namespace Jellyfin.Extensions
/// <param name="text">The string to check.</param>
/// <returns>True if the string has diacritics, false otherwise.</returns>
public static bool HasDiacritics(this string text)
- {
- return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
- }
+ => Diacritics.Extensions.StringExtensions.HasDiacritics(text)
+ || _nonConformingUnicode.IsMatch(text);
/// <summary>
/// Counts the number of occurrences of [needle] in the string.
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index 32f80812a..9a025d558 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -7,7 +7,7 @@
<!-- Code Analyzers-->
<ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index b11bdc477..fe4e57693 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -11,7 +11,7 @@
<!-- Code Analyzers-->
<ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index bd412bc76..6966d81d4 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -15,21 +15,21 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
+ <PackageReference Include="Moq" Version="4.18.4" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 3ca761b3d..5110d5917 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -12,19 +12,19 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 650973c6a..97350feda 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -12,19 +12,19 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index cba946800..a2ecd6083 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -7,19 +7,19 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
index 7730841a1..2a7e8fafd 100644
--- a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Extensions.Tests
{
var copy = strings.Reverse().ToArray();
Array.Sort(copy, new AlphanumericComparator());
- Assert.True(strings.SequenceEqual(copy));
+ Assert.Equal(strings, copy);
}
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index 075bcaac8..313192b24 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -7,13 +7,13 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
- <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2">
+ <PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@@ -22,7 +22,7 @@
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
index 903d88caa..69d20bd3f 100644
--- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
@@ -9,12 +9,15 @@ namespace Jellyfin.Extensions.Tests
[InlineData("", "")] // Identity edge-case (no diactritics)
[InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diactritics)
[InlineData("a\ud800b", "ab")] // Invalid UTF-16 char stripping
+ [InlineData("åäö", "aao")] // Issue #7484
[InlineData("Jön", "Jon")] // Issue #7484
[InlineData("Jönssonligan", "Jonssonligan")] // Issue #7484
[InlineData("Kieślowski", "Kieslowski")] // Issue #7450
[InlineData("Cidadão Kane", "Cidadao Kane")] // Issue #7560
[InlineData("운명처럼 널 사랑해", "운명처럼 널 사랑해")] // Issue #6393 (Korean language support)
[InlineData("애타는 로맨스", "애타는 로맨스")] // Issue #6393
+ [InlineData("Le cœur a ses raisons", "Le coeur a ses raisons")] // Issue #8893
+ [InlineData("Béla Tarr", "Bela Tarr")] // Issue #8893
public void RemoveDiacritics_ValidInput_Corrects(string input, string expectedResult)
{
string result = input.RemoveDiacritics();
@@ -25,12 +28,15 @@ namespace Jellyfin.Extensions.Tests
[InlineData("", false)] // Identity edge-case (no diactritics)
[InlineData("Indiana Jones", false)] // Identity (no diactritics)
[InlineData("a\ud800b", true)] // Invalid UTF-16 char stripping
+ [InlineData("åäö", true)] // Issue #7484
[InlineData("Jön", true)] // Issue #7484
[InlineData("Jönssonligan", true)] // Issue #7484
[InlineData("Kieślowski", true)] // Issue #7450
[InlineData("Cidadão Kane", true)] // Issue #7560
[InlineData("운명처럼 널 사랑해", false)] // Issue #6393 (Korean language support)
[InlineData("애타는 로맨스", false)] // Issue #6393
+ [InlineData("Le cœur a ses raisons", true)] // Issue #8893
+ [InlineData("Béla Tarr", true)] // Issue #8893
public void HasDiacritics_ValidInput_Corrects(string input, bool expectedResult)
{
bool result = input.HasDiacritics();
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
index f7163edc7..22b0c417b 100644
--- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
@@ -7,13 +7,13 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2">
+ <PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@@ -21,7 +21,7 @@
<!-- Code Analyzers -->
<ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
index 72bfb3fd2..373a54504 100644
--- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
@@ -8,13 +8,13 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2">
+ <PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@@ -22,7 +22,7 @@
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index e68e7f39a..a9a0dbc22 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -21,9 +21,9 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
@@ -33,7 +33,7 @@
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
index 243127438..9ace80bbd 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
@@ -26,7 +26,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Path = "/media/sub.ass",
IsExternal = true
},
- new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
+ new SubtitleEncoder.SubtitleInfo()
+ {
+ Path = "/media/sub.ass",
+ Protocol = MediaProtocol.File,
+ Format = "ass",
+ IsExternal = true
+ });
data.Add(
new MediaSourceInfo()
@@ -38,7 +44,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Path = "/media/sub.ssa",
IsExternal = true
},
- new SubtitleEncoder.SubtitleInfo("/media/sub.ssa", MediaProtocol.File, "ssa", true));
+ new SubtitleEncoder.SubtitleInfo()
+ {
+ Path = "/media/sub.ssa",
+ Protocol = MediaProtocol.File,
+ Format = "ssa",
+ IsExternal = true
+ });
data.Add(
new MediaSourceInfo()
@@ -50,7 +62,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Path = "/media/sub.srt",
IsExternal = true
},
- new SubtitleEncoder.SubtitleInfo("/media/sub.srt", MediaProtocol.File, "srt", true));
+ new SubtitleEncoder.SubtitleInfo()
+ {
+ Path = "/media/sub.srt",
+ Protocol = MediaProtocol.File,
+ Format = "srt",
+ IsExternal = true
+ });
data.Add(
new MediaSourceInfo()
@@ -62,14 +80,20 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Path = "/media/sub.ass",
IsExternal = true
},
- new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
+ new SubtitleEncoder.SubtitleInfo()
+ {
+ Path = "/media/sub.ass",
+ Protocol = MediaProtocol.File,
+ Format = "ass",
+ IsExternal = true
+ });
return data;
}
[Theory]
[MemberData(nameof(GetReadableFile_Valid_TestData))]
- internal async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo)
+ public async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo)
{
var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
var subtitleEncoder = fixture.Create<SubtitleEncoder>();
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 5e11a7232..60be17a74 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -164,7 +164,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
{
- var options = await GetVideoOptions(deviceName, mediaSource);
+ var options = await GetMediaOptions(deviceName, mediaSource);
BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
}
@@ -262,7 +262,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
{
- var options = await GetVideoOptions(deviceName, mediaSource);
+ var options = await GetMediaOptions(deviceName, mediaSource);
options.AudioStreamIndex = 1;
options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1;
@@ -298,7 +298,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
{
- var options = await GetVideoOptions(deviceName, mediaSource);
+ var options = await GetMediaOptions(deviceName, mediaSource);
var streamCount = options.MediaSources[0].MediaStreams.Count;
if (streamCount > 0)
{
@@ -311,7 +311,7 @@ namespace Jellyfin.Model.Tests
Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex);
}
- private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol)
+ private StreamInfo? BuildVideoItemSimpleTest(MediaOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol)
{
if (string.IsNullOrEmpty(transcodeProtocol))
{
@@ -320,28 +320,28 @@ namespace Jellyfin.Model.Tests
var builder = GetStreamBuilder();
- var val = builder.BuildVideoItem(options);
- Assert.NotNull(val);
+ var streamInfo = builder.GetOptimalVideoStream(options);
+ Assert.NotNull(streamInfo);
if (playMethod is not null)
{
- Assert.Equal(playMethod, val.PlayMethod);
+ Assert.Equal(playMethod, streamInfo.PlayMethod);
}
- Assert.Equal(why, val.TranscodeReasons);
+ Assert.Equal(why, streamInfo.TranscodeReasons);
var audioStreamIndexInput = options.AudioStreamIndex;
- var targetVideoStream = val.TargetVideoStream;
- var targetAudioStream = val.TargetAudioStream;
+ var targetVideoStream = streamInfo.TargetVideoStream;
+ var targetAudioStream = streamInfo.TargetAudioStream;
- var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId);
+ var mediaSource = options.MediaSources.First(source => source.Id == streamInfo.MediaSourceId);
Assert.NotNull(mediaSource);
var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video);
var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio);
// TODO: Check AudioStreamIndex vs options.AudioStreamIndex
var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex);
- var uri = ParseUri(val);
+ var uri = ParseUri(streamInfo);
if (playMethod == PlayMethod.DirectPlay)
{
@@ -351,98 +351,99 @@ namespace Jellyfin.Model.Tests
// Assert.Contains(uri.Extension, containers);
// Check expected video codec (1)
- Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
- Assert.Single(val.TargetVideoCodec);
+ Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec);
+ Assert.Single(streamInfo.TargetVideoCodec);
// Check expected audio codecs (1)
- Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec);
- Assert.Single(val.TargetAudioCodec);
+ Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec);
+ Assert.Single(streamInfo.TargetAudioCodec);
// Assert.Single(val.AudioCodecs);
if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
{
- Assert.Equal(val.Container, uri.Extension);
+ Assert.Equal(streamInfo.Container, uri.Extension);
}
}
else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode)
{
- Assert.NotNull(val.Container);
- Assert.NotEmpty(val.VideoCodecs);
- Assert.NotEmpty(val.AudioCodecs);
+ Assert.NotNull(streamInfo.Container);
+ Assert.NotEmpty(streamInfo.VideoCodecs);
+ Assert.NotEmpty(streamInfo.AudioCodecs);
// Check expected container (todo: this could be a test param)
if (transcodeProtocol.Equals("http", StringComparison.Ordinal))
{
// Assert.Equal("webm", val.Container);
- Assert.Equal(val.Container, uri.Extension);
+ Assert.Equal(streamInfo.Container, uri.Extension);
Assert.Equal("stream", uri.Filename);
- Assert.Equal("http", val.SubProtocol);
+ Assert.Equal("http", streamInfo.SubProtocol);
}
else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal))
{
- Assert.Equal("mp4", val.Container);
+ Assert.Equal("mp4", streamInfo.Container);
Assert.Equal("m3u8", uri.Extension);
Assert.Equal("master", uri.Filename);
- Assert.Equal("hls", val.SubProtocol);
+ Assert.Equal("hls", streamInfo.SubProtocol);
}
else
{
- Assert.Equal("ts", val.Container);
+ Assert.Equal("ts", streamInfo.Container);
Assert.Equal("m3u8", uri.Extension);
Assert.Equal("master", uri.Filename);
- Assert.Equal("hls", val.SubProtocol);
+ Assert.Equal("hls", streamInfo.SubProtocol);
}
// Full transcode
if (transcodeMode.Equals("Transcode", StringComparison.Ordinal))
{
- if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0)
+ if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0)
{
Assert.All(
videoStreams,
- stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs));
+ stream => Assert.DoesNotContain(stream.Codec, streamInfo.VideoCodecs));
}
- // TODO: Fill out tests here
+ // TODO: fill out tests here
}
// DirectStream and Remux
else
{
// Check expected video codec (1)
- Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
- Assert.Single(val.TargetVideoCodec);
+ Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec);
+ Assert.Single(streamInfo.TargetVideoCodec);
if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
{
// Check expected audio codecs (1)
if (!targetAudioStream.IsExternal)
{
- if (val.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported))
+ // Check expected audio codecs (1)
+ if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported))
{
- Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
+ Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs);
}
else
{
- Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+ Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
}
}
}
else if (transcodeMode.Equals("Remux", StringComparison.Ordinal))
{
// Check expected audio codecs (1)
- Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
- Assert.Single(val.AudioCodecs);
+ Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs);
+ Assert.Single(streamInfo.AudioCodecs);
}
// Video details
var videoStream = targetVideoStream;
- Assert.False(val.EstimateContentLength);
- Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
- Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>());
- Assert.Equal(videoStream.Level, val.TargetVideoLevel);
- Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth);
- Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
+ Assert.False(streamInfo.EstimateContentLength);
+ Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo);
+ Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>());
+ Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel);
+ Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth);
+ Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
// Audio codec not supported
if ((why & TranscodeReason.AudioCodecNotSupported) != 0)
@@ -453,7 +454,7 @@ namespace Jellyfin.Model.Tests
// TODO:fixme
if (!targetAudioStream.IsExternal)
{
- Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+ Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
}
}
@@ -465,7 +466,7 @@ namespace Jellyfin.Model.Tests
{
if (!stream.IsExternal)
{
- Assert.DoesNotContain(stream.Codec, val.AudioCodecs);
+ Assert.DoesNotContain(stream.Codec, streamInfo.AudioCodecs);
}
});
}
@@ -474,14 +475,14 @@ namespace Jellyfin.Model.Tests
}
else if (playMethod is null)
{
- Assert.Null(val.SubProtocol);
+ Assert.Null(streamInfo.SubProtocol);
Assert.Equal("stream", uri.Filename);
- Assert.False(val.EstimateContentLength);
- Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
+ Assert.False(streamInfo.EstimateContentLength);
+ Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo);
}
- return val;
+ return streamInfo;
}
private static async ValueTask<T> TestData<T>(string name)
@@ -507,7 +508,7 @@ namespace Jellyfin.Model.Tests
return new StreamBuilder(transcodeSupport.Object, logger);
}
- private static async ValueTask<VideoOptions> GetVideoOptions(string deviceProfile, params string[] sources)
+ private static async ValueTask<MediaOptions> GetMediaOptions(string deviceProfile, params string[] sources)
{
var mediaSources = sources.Select(src => TestData<MediaSourceInfo>(src))
.Select(val => val.Result)
@@ -516,7 +517,7 @@ namespace Jellyfin.Model.Tests
var dp = await TestData<DeviceProfile>(deviceProfile);
- return new VideoOptions()
+ return new MediaOptions()
{
ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"),
MediaSourceId = mediaSourceId,
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index 2c7e393af..9858623f8 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -7,14 +7,14 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
</ItemGroup>
@@ -26,7 +26,7 @@
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 0d9acf0e1..920f490ed 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -12,14 +12,14 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
@@ -28,7 +28,7 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
index 1574bce58..6c9c98cbe 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
@@ -10,6 +10,7 @@ namespace Jellyfin.Naming.Tests.Video
[Theory]
[InlineData("Super movie 480p.mp4", "Super movie")]
+ [InlineData("Super movie Multi.mp4", "Super movie")]
[InlineData("Super movie 480p 2001.mp4", "Super movie")]
[InlineData("Super movie [480p].mp4", "Super movie")]
[InlineData("480 Super movie [tmdbid=12345].mp4", "480 Super movie")]
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 9e13dd4ad..74bf7cb0e 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -12,20 +12,20 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="Moq" Version="4.18.4" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index 2d8e3c8f2..d3292c38e 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -13,14 +13,14 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2">
+ <PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@@ -28,7 +28,7 @@
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index d91b4f00b..b796e07d1 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -21,20 +21,20 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs
index 538010f6c..07feae587 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs
@@ -51,4 +51,68 @@ public class MediaStreamSelectorTests
Assert.Equal(expectedIndex, MediaStreamSelector.GetDefaultAudioStreamIndex(streams, preferredLanguages, preferDefaultTrack));
}
+
+ public static TheoryData<MediaStream, int> GetStreamScore_MediaStream_TestData()
+ {
+ var data = new TheoryData<MediaStream, int>();
+
+ data.Add(new MediaStream(), 111111);
+ data.Add(
+ new MediaStream()
+ {
+ Language = "eng"
+ },
+ 10111111);
+ data.Add(
+ new MediaStream()
+ {
+ Language = "fre"
+ },
+ 10011111);
+ data.Add(
+ new MediaStream()
+ {
+ IsForced = true
+ },
+ 121111);
+ data.Add(
+ new MediaStream()
+ {
+ IsDefault = true
+ },
+ 112111);
+ data.Add(
+ new MediaStream()
+ {
+ SupportsExternalStream = true
+ },
+ 111211);
+ data.Add(
+ new MediaStream()
+ {
+ IsExternal = true
+ },
+ 111112);
+ data.Add(
+ new MediaStream()
+ {
+ Language = "eng",
+ IsForced = true,
+ IsDefault = true,
+ SupportsExternalStream = true,
+ IsExternal = true
+ },
+ 10122212);
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetStreamScore_MediaStream_TestData))]
+ public void GetStreamScore_MediaStream_CorrectScore(MediaStream stream, int expectedScore)
+ {
+ var languagePref = new[] { "eng", "fre" };
+
+ Assert.Equal(expectedScore, MediaStreamSelector.GetStreamScore(stream, languagePref));
+ }
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 3e7d6ed1d..16eb7a75c 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var cultures = localizationManager.GetCultures().ToList();
- Assert.Equal(190, cultures.Count);
+ Assert.Equal(191, cultures.Count);
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
Assert.NotNull(germany);
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index ecc3ebb86..c40f6942b 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -9,17 +9,17 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
+ <PackageReference Include="Moq" Version="4.18.4" />
</ItemGroup>
<ItemGroup>
@@ -31,7 +31,7 @@
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index c38faeda1..55bc43455 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -4,12 +4,16 @@ using System.Globalization;
using System.IO;
using System.Threading;
using Emby.Server.Implementations;
+using Jellyfin.Server.Extensions;
+using Jellyfin.Server.Helpers;
using MediaBrowser.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
@@ -32,7 +36,7 @@ namespace Jellyfin.Server.Integration.Tests
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(formatProvider: CultureInfo.InvariantCulture)
.CreateLogger();
- Program.PerformStaticInitialization();
+ StartupHelpers.PerformStaticInitialization();
}
/// <inheritdoc/>
@@ -62,7 +66,7 @@ namespace Jellyfin.Server.Integration.Tests
// Create the logging config file
// TODO: We shouldn't need to do this since we are only logging to console
- Program.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult();
+ StartupHelpers.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult();
// Create a copy of the application configuration to use for startup
var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths);
@@ -78,11 +82,17 @@ namespace Jellyfin.Server.Integration.Tests
commandLineOpts,
startupConfig);
_disposableComponents.Add(appHost);
- var serviceCollection = new ServiceCollection();
- appHost.Init(serviceCollection);
- // Configure the web host builder
- Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths);
+ builder.ConfigureServices(services => appHost.Init(services))
+ .ConfigureWebHostBuilder(appHost, startupConfig, appPaths, NullLogger.Instance)
+ .ConfigureAppConfiguration((context, builder) =>
+ {
+ builder
+ .SetBasePath(appPaths.ConfigurationDirectoryPath)
+ .AddInMemoryCollection(ConfigurationOptions.DefaultConfiguration)
+ .AddEnvironmentVariables("JELLYFIN_")
+ .AddInMemoryCollection(commandLineOpts.ConvertToConfig());
+ });
}
/// <inheritdoc/>
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index 0ce2721c7..a72a6f185 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -10,21 +10,21 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
+ <PackageReference Include="Moq" Version="4.18.4" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
index d15c9d6f5..797fc8f64 100644
--- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
+++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
-using Jellyfin.Server.Middleware;
+using Jellyfin.Api.Middleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index bde34d639..dc5b5b9e6 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -13,19 +13,19 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
- <PackageReference Include="Moq" Version="4.18.2" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+ <PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.1.2" />
+ <PackageReference Include="coverlet.collector" Version="3.2.0" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>