فهرست منبع

Merge branch 'master' into chromecast-config

# Conflicts:
#	Emby.Server.Implementations/ApplicationHost.cs
Cody Robibero 1 سال پیش
والد
کامیت
6bd6fb6e0a
97فایلهای تغییر یافته به همراه1543 افزوده شده و 2263 حذف شده
  1. 3 3
      .github/workflows/codeql-analysis.yml
  2. 7 6
      .github/workflows/repo-stale.yaml
  3. 1 0
      CONTRIBUTORS.md
  4. 5 5
      Directory.Packages.props
  5. 1 1
      Dockerfile
  6. 1 1
      Dockerfile.arm
  7. 1 1
      Dockerfile.arm64
  8. 1 1
      Emby.Dlna/DlnaManager.cs
  9. 1 1
      Emby.Naming/ExternalFiles/ExternalPathParser.cs
  10. 3 4
      Emby.Naming/Video/StubResolver.cs
  11. 1 1
      Emby.Photos/PhotoProvider.cs
  12. 7 13
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  13. 14 175
      Emby.Server.Implementations/ApplicationHost.cs
  14. 6 5
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  15. 12 13
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  16. 4 0
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  17. 16 15
      Emby.Server.Implementations/Library/LibraryManager.cs
  18. 3 3
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  19. 1 1
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  20. 4 5
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  21. 13 14
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  22. 1 8
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  23. 43 1
      Emby.Server.Implementations/Localization/Core/as.json
  24. 52 0
      Emby.Server.Implementations/Localization/Core/chr.json
  25. 24 24
      Emby.Server.Implementations/Localization/Core/da.json
  26. 2 2
      Emby.Server.Implementations/Localization/Core/es.json
  27. 2 2
      Emby.Server.Implementations/Localization/Core/fr.json
  28. 121 1
      Emby.Server.Implementations/Localization/Core/kn.json
  29. 3 1
      Emby.Server.Implementations/Localization/Core/ml.json
  30. 1 1
      Emby.Server.Implementations/Localization/Core/ms.json
  31. 4 1
      Emby.Server.Implementations/Localization/Core/pr.json
  32. 10 1
      Emby.Server.Implementations/Localization/Core/zu.json
  33. 1 1
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  34. 6 10
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  35. 10 12
      Emby.Server.Implementations/Plugins/PluginManager.cs
  36. 74 81
      Emby.Server.Implementations/Session/SessionManager.cs
  37. 109 0
      Emby.Server.Implementations/SystemManager.cs
  38. 1 2
      Emby.Server.Implementations/Updates/InstallationManager.cs
  39. 23 18
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  40. 5 4
      Jellyfin.Api/Controllers/HlsSegmentController.cs
  41. 19 24
      Jellyfin.Api/Controllers/ImageController.cs
  42. 1 6
      Jellyfin.Api/Controllers/LiveTvController.cs
  43. 4 4
      Jellyfin.Api/Controllers/SubtitleController.cs
  44. 22 34
      Jellyfin.Api/Controllers/SystemController.cs
  45. 1 34
      Jellyfin.Api/Helpers/DynamicHlsHelper.cs
  46. 5 3
      Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
  47. 10 5
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  48. 1 1
      Jellyfin.Api/Helpers/TranscodingJobHelper.cs
  49. 1 2
      Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
  50. 36 0
      Jellyfin.Data/Enums/PersonKind.cs
  51. 5 25
      Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
  52. 2 8
      Jellyfin.Server.Implementations/Users/UserManager.cs
  53. 0 6
      Jellyfin.Server/CoreAppHost.cs
  54. 1 0
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  55. 1 2
      Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
  56. 8 71
      Jellyfin.Server/Program.cs
  57. 4 56
      MediaBrowser.Common/Extensions/ProcessExtensions.cs
  58. 4 17
      MediaBrowser.Common/IApplicationHost.cs
  59. 0 5
      MediaBrowser.Common/Plugins/IPluginManager.cs
  60. 0 11
      MediaBrowser.Controller/Drawing/IImageProcessor.cs
  61. 2 1
      MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
  62. 12 30
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  63. 193 0
      MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
  64. 0 12
      MediaBrowser.Controller/IServerApplicationHost.cs
  65. 34 0
      MediaBrowser.Controller/ISystemManager.cs
  66. 32 10
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  67. 1 1
      MediaBrowser.Controller/Resolvers/IItemResolver.cs
  68. 2 4
      MediaBrowser.Controller/Resolvers/ItemResolver.cs
  69. 0 14
      MediaBrowser.Controller/Session/ISessionManager.cs
  70. 62 443
      MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
  71. 2 7
      MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
  72. 20 60
      MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
  73. 26 26
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  74. 1 0
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  75. 16 30
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  76. 17 0
      MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
  77. 0 2
      MediaBrowser.Model/IO/IFileSystem.cs
  78. 5 1
      MediaBrowser.Providers/Manager/ImageSaver.cs
  79. 0 6
      MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
  80. 245 606
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
  81. 61 114
      MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
  82. 11 18
      MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
  83. 6 23
      MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
  84. 5 17
      MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
  85. 2 2
      MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
  86. 1 0
      debian/jellyfin.init
  87. 1 1
      fedora/jellyfin.service
  88. 1 0
      fedora/jellyfin.spec
  89. 3 13
      src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
  90. 6 6
      src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
  91. 2 54
      src/Jellyfin.Drawing/ImageProcessor.cs
  92. 13 0
      tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
  93. 1 0
      tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
  94. 8 5
      tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
  95. 26 0
      tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs
  96. 1 2
      tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
  97. 3 3
      tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs

+ 3 - 3
.github/workflows/codeql-analysis.yml

@@ -27,11 +27,11 @@ jobs:
         dotnet-version: '7.0.x'
         dotnet-version: '7.0.x'
 
 
     - name: Initialize CodeQL
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8
+      uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
       with:
       with:
         languages: ${{ matrix.language }}
         languages: ${{ matrix.language }}
         queries: +security-extended
         queries: +security-extended
     - name: Autobuild
     - name: Autobuild
-      uses: github/codeql-action/autobuild@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8
+      uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
     - name: Perform CodeQL Analysis
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8
+      uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1

+ 7 - 6
.github/workflows/repo-stale.yaml

@@ -2,16 +2,17 @@ name: Stale Check
 
 
 on:
 on:
   schedule:
   schedule:
-    - cron: '30 1 * * *'
+    - cron: '30 */12 * * *'
   workflow_dispatch:
   workflow_dispatch:
 
 
 permissions:
 permissions:
   issues: write
   issues: write
   pull-requests: write
   pull-requests: write
+  actions: write
 
 
 jobs:
 jobs:
   issues:
   issues:
-    name: Check issues
+    name: Check for stale issues
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     if: ${{ contains(github.repository, 'jellyfin/') }}
     if: ${{ contains(github.repository, 'jellyfin/') }}
     steps:
     steps:
@@ -26,11 +27,11 @@ jobs:
           exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
           exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
           stale-issue-label: stale
           stale-issue-label: stale
           stale-issue-message: |-
           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.
+            This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.  
 
 
-            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).
+            If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
+          close-issue-message: |-
+            This issue was closed due to inactivity.
 
 
   prs-conflicts:
   prs-conflicts:
     name: Check PRs with merge conflicts
     name: Check PRs with merge conflicts

+ 1 - 0
CONTRIBUTORS.md

@@ -238,3 +238,4 @@
  - [Jakob Kukla](https://github.com/jakobkukla)
  - [Jakob Kukla](https://github.com/jakobkukla)
  - [Utku Özdemir](https://github.com/utkuozdemir)
  - [Utku Özdemir](https://github.com/utkuozdemir)
  - [JPUC1143](https://github.com/Jpuc1143/)
  - [JPUC1143](https://github.com/Jpuc1143/)
+ - [0x25CBFC4F](https://github.com/0x25CBFC4F)

+ 5 - 5
Directory.Packages.props

@@ -19,12 +19,13 @@
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
     <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
     <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
     <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
     <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
+    <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
     <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
     <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
     <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
     <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
     <PackageVersion Include="libse" Version="3.6.13" />
     <PackageVersion Include="libse" Version="3.6.13" />
     <PackageVersion Include="LrcParser" Version="2023.524.0" />
     <PackageVersion Include="LrcParser" Version="2023.524.0" />
     <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
     <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
-    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" />
+    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
     <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
     <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
     <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
     <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
     <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
     <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
@@ -65,14 +66,13 @@
     <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
     <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
     <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
     <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
     <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
     <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
-    <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" />
+    <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
     <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
     <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
     <PackageVersion Include="SharpFuzz" Version="2.1.1" />
     <PackageVersion Include="SharpFuzz" Version="2.1.1" />
+    <PackageVersion Include="SkiaSharp" Version="2.88.5" />
+    <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
     <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
     <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
     <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
     <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
-    <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" />
-    <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
-    <PackageVersion Include="SkiaSharp" Version="2.88.6" />
     <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
     <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
     <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
     <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
     <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
     <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />

+ 1 - 1
Dockerfile

@@ -4,7 +4,7 @@
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 ARG DOTNET_VERSION=7.0
 ARG DOTNET_VERSION=7.0
 
 
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

+ 1 - 1
Dockerfile.arm

@@ -5,7 +5,7 @@
 ARG DOTNET_VERSION=7.0
 ARG DOTNET_VERSION=7.0
 
 
 
 
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

+ 1 - 1
Dockerfile.arm64

@@ -5,7 +5,7 @@
 ARG DOTNET_VERSION=7.0
 ARG DOTNET_VERSION=7.0
 
 
 
 
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

+ 1 - 1
Emby.Dlna/DlnaManager.cs

@@ -228,7 +228,7 @@ namespace Emby.Dlna
             try
             try
             {
             {
                 return _fileSystem.GetFilePaths(path)
                 return _fileSystem.GetFilePaths(path)
-                    .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
+                    .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
                     .Select(i => ParseProfileFile(i, type))
                     .Select(i => ParseProfileFile(i, type))
                     .Where(i => i is not null)
                     .Where(i => i is not null)
                     .ToList()!; // We just filtered out all the nulls
                     .ToList()!; // We just filtered out all the nulls

+ 1 - 1
Emby.Naming/ExternalFiles/ExternalPathParser.cs

@@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
                 return null;
                 return null;
             }
             }
 
 
-            var extension = Path.GetExtension(path);
+            var extension = Path.GetExtension(path.AsSpan());
             if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
                 && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
                 && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
             {
             {

+ 3 - 4
Emby.Naming/Video/StubResolver.cs

@@ -26,19 +26,18 @@ namespace Emby.Naming.Video
                 return false;
                 return false;
             }
             }
 
 
-            var extension = Path.GetExtension(path);
+            var extension = Path.GetExtension(path.AsSpan());
 
 
             if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return false;
                 return false;
             }
             }
 
 
-            path = Path.GetFileNameWithoutExtension(path);
-            var token = Path.GetExtension(path).TrimStart('.');
+            var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
 
 
             foreach (var rule in options.StubTypes)
             foreach (var rule in options.StubTypes)
             {
             {
-                if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
+                if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     stubType = rule.StubType;
                     stubType = rule.StubType;
                     return true;
                     return true;

+ 1 - 1
Emby.Photos/PhotoProvider.cs

@@ -61,7 +61,7 @@ namespace Emby.Photos
             item.SetImagePath(ImageType.Primary, item.Path);
             item.SetImagePath(ImageType.Primary, item.Path);
 
 
             // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
             // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
-            if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
+            if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
             {
             {
                 try
                 try
                 {
                 {

+ 7 - 13
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
     /// </summary>
     /// </summary>
     public abstract class BaseConfigurationManager : IConfigurationManager
     public abstract class BaseConfigurationManager : IConfigurationManager
     {
     {
-        private readonly IFileSystem _fileSystem;
-
-        private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
-
-        /// <summary>
-        /// The _configuration sync lock.
-        /// </summary>
-        private readonly object _configurationSyncLock = new object();
+        private readonly ConcurrentDictionary<string, object> _configurations = new();
+        private readonly object _configurationSyncLock = new();
 
 
         private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
         private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
         private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
         private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
         /// <param name="applicationPaths">The application paths.</param>
         /// <param name="applicationPaths">The application paths.</param>
         /// <param name="loggerFactory">The logger factory.</param>
         /// <param name="loggerFactory">The logger factory.</param>
         /// <param name="xmlSerializer">The XML serializer.</param>
         /// <param name="xmlSerializer">The XML serializer.</param>
-        /// <param name="fileSystem">The file system.</param>
-        protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
+        protected BaseConfigurationManager(
+            IApplicationPaths applicationPaths,
+            ILoggerFactory loggerFactory,
+            IXmlSerializer xmlSerializer)
         {
         {
             CommonApplicationPaths = applicationPaths;
             CommonApplicationPaths = applicationPaths;
             XmlSerializer = xmlSerializer;
             XmlSerializer = xmlSerializer;
-            _fileSystem = fileSystem;
             Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
             Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
 
 
             UpdateCachePath();
             UpdateCachePath();
@@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
         {
         {
             var file = Path.Combine(path, Guid.NewGuid().ToString());
             var file = Path.Combine(path, Guid.NewGuid().ToString());
             File.WriteAllText(file, string.Empty);
             File.WriteAllText(file, string.Empty);
-            _fileSystem.DeleteFile(file);
+            File.Delete(file);
         }
         }
 
 
         private string GetConfigurationFile(string key)
         private string GetConfigurationFile(string key)

+ 14 - 175
Emby.Server.Implementations/ApplicationHost.cs

@@ -12,7 +12,6 @@ using System.Linq;
 using System.Net;
 using System.Net;
 using System.Reflection;
 using System.Reflection;
 using System.Security.Cryptography.X509Certificates;
 using System.Security.Cryptography.X509Certificates;
-using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Dlna;
 using Emby.Dlna;
 using Emby.Dlna.Main;
 using Emby.Dlna.Main;
@@ -112,7 +111,7 @@ namespace Emby.Server.Implementations
     /// <summary>
     /// <summary>
     /// Class CompositionRoot.
     /// Class CompositionRoot.
     /// </summary>
     /// </summary>
-    public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
+    public abstract class ApplicationHost : IServerApplicationHost, IDisposable
     {
     {
         /// <summary>
         /// <summary>
         /// The disposable parts.
         /// The disposable parts.
@@ -120,14 +119,12 @@ namespace Emby.Server.Implementations
         private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
         private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
         private readonly DeviceId _deviceId;
         private readonly DeviceId _deviceId;
 
 
-        private readonly IFileSystem _fileSystemManager;
         private readonly IConfiguration _startupConfig;
         private readonly IConfiguration _startupConfig;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IStartupOptions _startupOptions;
         private readonly IStartupOptions _startupOptions;
         private readonly IPluginManager _pluginManager;
         private readonly IPluginManager _pluginManager;
 
 
         private List<Type> _creatingInstances;
         private List<Type> _creatingInstances;
-        private ISessionManager _sessionManager;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets all concrete types.
         /// Gets or sets all concrete types.
@@ -135,7 +132,7 @@ namespace Emby.Server.Implementations
         /// <value>All concrete types.</value>
         /// <value>All concrete types.</value>
         private Type[] _allConcreteTypes;
         private Type[] _allConcreteTypes;
 
 
-        private bool _disposed = false;
+        private bool _disposed;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
         /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -154,10 +151,8 @@ namespace Emby.Server.Implementations
             LoggerFactory = loggerFactory;
             LoggerFactory = loggerFactory;
             _startupOptions = options;
             _startupOptions = options;
             _startupConfig = startupConfig;
             _startupConfig = startupConfig;
-            _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
 
 
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
-            _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler());
             _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
             _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
 
 
             ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
             ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
@@ -165,13 +160,15 @@ namespace Emby.Server.Implementations
             ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
             ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
 
 
             _xmlSerializer = new MyXmlSerializer();
             _xmlSerializer = new MyXmlSerializer();
-            ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+            ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
             _pluginManager = new PluginManager(
             _pluginManager = new PluginManager(
                 LoggerFactory.CreateLogger<PluginManager>(),
                 LoggerFactory.CreateLogger<PluginManager>(),
                 this,
                 this,
                 ConfigurationManager.Configuration,
                 ConfigurationManager.Configuration,
                 ApplicationPaths.PluginsPath,
                 ApplicationPaths.PluginsPath,
                 ApplicationVersion);
                 ApplicationVersion);
+
+            _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -186,23 +183,16 @@ namespace Emby.Server.Implementations
 
 
         public bool CoreStartupHasCompleted { get; private set; }
         public bool CoreStartupHasCompleted { get; private set; }
 
 
-        public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
-            && !_startupOptions.IsService
-            && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
-
         /// <summary>
         /// <summary>
         /// Gets the <see cref="INetworkManager"/> singleton instance.
         /// Gets the <see cref="INetworkManager"/> singleton instance.
         /// </summary>
         /// </summary>
         public INetworkManager NetManager { get; private set; }
         public INetworkManager NetManager { get; private set; }
 
 
-        /// <summary>
-        /// Gets a value indicating whether this instance has changes that require the entire application to restart.
-        /// </summary>
-        /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
+        /// <inheritdoc />
         public bool HasPendingRestart { get; private set; }
         public bool HasPendingRestart { get; private set; }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public bool IsShuttingDown { get; private set; }
+        public bool ShouldRestart { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the logger.
         /// Gets the logger.
@@ -406,11 +396,9 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// <summary>
         /// Runs the startup tasks.
         /// Runs the startup tasks.
         /// </summary>
         /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns><see cref="Task" />.</returns>
         /// <returns><see cref="Task" />.</returns>
-        public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
+        public async Task RunStartupTasksAsync()
         {
         {
-            cancellationToken.ThrowIfCancellationRequested();
             Logger.LogInformation("Running startup tasks");
             Logger.LogInformation("Running startup tasks");
 
 
             Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
             Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@@ -424,8 +412,6 @@ namespace Emby.Server.Implementations
 
 
             var entryPoints = GetExports<IServerEntryPoint>();
             var entryPoints = GetExports<IServerEntryPoint>();
 
 
-            cancellationToken.ThrowIfCancellationRequested();
-
             var stopWatch = new Stopwatch();
             var stopWatch = new Stopwatch();
             stopWatch.Start();
             stopWatch.Start();
 
 
@@ -435,8 +421,6 @@ namespace Emby.Server.Implementations
             Logger.LogInformation("Core startup complete");
             Logger.LogInformation("Core startup complete");
             CoreStartupHasCompleted = true;
             CoreStartupHasCompleted = true;
 
 
-            cancellationToken.ThrowIfCancellationRequested();
-
             stopWatch.Restart();
             stopWatch.Restart();
 
 
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@@ -509,7 +493,11 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton(_pluginManager);
             serviceCollection.AddSingleton(_pluginManager);
             serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
             serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 
 
-            serviceCollection.AddSingleton(_fileSystemManager);
+            serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
+            serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
+
+            serviceCollection.AddScoped<ISystemManager, SystemManager>();
+
             serviceCollection.AddSingleton<TmdbClientManager>();
             serviceCollection.AddSingleton<TmdbClientManager>();
 
 
             serviceCollection.AddSingleton(NetManager);
             serviceCollection.AddSingleton(NetManager);
@@ -633,8 +621,6 @@ namespace Emby.Server.Implementations
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             await localizationManager.LoadAll().ConfigureAwait(false);
             await localizationManager.LoadAll().ConfigureAwait(false);
 
 
-            _sessionManager = Resolve<ISessionManager>();
-
             SetStaticProperties();
             SetStaticProperties();
 
 
             FindParts();
             FindParts();
@@ -685,7 +671,7 @@ namespace Emby.Server.Implementations
             BaseItem.ProviderManager = Resolve<IProviderManager>();
             BaseItem.ProviderManager = Resolve<IProviderManager>();
             BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
             BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
             BaseItem.ItemRepository = Resolve<IItemRepository>();
             BaseItem.ItemRepository = Resolve<IItemRepository>();
-            BaseItem.FileSystem = _fileSystemManager;
+            BaseItem.FileSystem = Resolve<IFileSystem>();
             BaseItem.UserDataManager = Resolve<IUserDataManager>();
             BaseItem.UserDataManager = Resolve<IUserDataManager>();
             BaseItem.ChannelManager = Resolve<IChannelManager>();
             BaseItem.ChannelManager = Resolve<IChannelManager>();
             Video.LiveTvManager = Resolve<ILiveTvManager>();
             Video.LiveTvManager = Resolve<ILiveTvManager>();
@@ -855,38 +841,6 @@ namespace Emby.Server.Implementations
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Restarts this instance.
-        /// </summary>
-        public void Restart()
-        {
-            if (IsShuttingDown)
-            {
-                return;
-            }
-
-            IsShuttingDown = true;
-            _pluginManager.UnloadAssemblies();
-
-            Task.Run(async () =>
-            {
-                try
-                {
-                    await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    Logger.LogError(ex, "Error sending server restart notification");
-                }
-
-                Logger.LogInformation("Calling RestartInternal");
-
-                RestartInternal();
-            });
-        }
-
-        protected abstract void RestartInternal();
-
         /// <summary>
         /// <summary>
         /// Gets the composable part assemblies.
         /// Gets the composable part assemblies.
         /// </summary>
         /// </summary>
@@ -942,50 +896,6 @@ namespace Emby.Server.Implementations
 
 
         protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
         protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
 
 
-        /// <summary>
-        /// Gets the system status.
-        /// </summary>
-        /// <param name="request">Where this request originated.</param>
-        /// <returns>SystemInfo.</returns>
-        public SystemInfo GetSystemInfo(HttpRequest request)
-        {
-            return new SystemInfo
-            {
-                HasPendingRestart = HasPendingRestart,
-                IsShuttingDown = IsShuttingDown,
-                Version = ApplicationVersionString,
-                WebSocketPortNumber = HttpPort,
-                CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
-                Id = SystemId,
-                ProgramDataPath = ApplicationPaths.ProgramDataPath,
-                WebPath = ApplicationPaths.WebPath,
-                LogPath = ApplicationPaths.LogDirectoryPath,
-                ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
-                InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
-                CachePath = ApplicationPaths.CachePath,
-                CanLaunchWebBrowser = CanLaunchWebBrowser,
-                TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
-                ServerName = FriendlyName,
-                LocalAddress = GetSmartApiUrl(request),
-                SupportsLibraryMonitor = true,
-                PackageName = _startupOptions.PackageName,
-                CastReceiverApplications = ConfigurationManager.Configuration.CastReceiverApplications
-            };
-        }
-
-        public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
-        {
-            return new PublicSystemInfo
-            {
-                Version = ApplicationVersionString,
-                ProductName = ApplicationProductName,
-                Id = SystemId,
-                ServerName = FriendlyName,
-                LocalAddress = GetSmartApiUrl(request),
-                StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
-            };
-        }
-
         /// <inheritdoc/>
         /// <inheritdoc/>
         public string GetSmartApiUrl(IPAddress remoteAddr)
         public string GetSmartApiUrl(IPAddress remoteAddr)
         {
         {
@@ -1066,30 +976,6 @@ namespace Emby.Server.Implementations
             }.ToString().TrimEnd('/');
             }.ToString().TrimEnd('/');
         }
         }
 
 
-        /// <inheritdoc />
-        public async Task Shutdown()
-        {
-            if (IsShuttingDown)
-            {
-                return;
-            }
-
-            IsShuttingDown = true;
-
-            try
-            {
-                await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error sending server shutdown notification");
-            }
-
-            ShutdownInternal();
-        }
-
-        protected abstract void ShutdownInternal();
-
         public IEnumerable<Assembly> GetApiPluginAssemblies()
         public IEnumerable<Assembly> GetApiPluginAssemblies()
         {
         {
             var assemblies = _allConcreteTypes
             var assemblies = _allConcreteTypes
@@ -1153,52 +1039,5 @@ namespace Emby.Server.Implementations
 
 
             _disposed = true;
             _disposed = true;
         }
         }
-
-        public async ValueTask DisposeAsync()
-        {
-            await DisposeAsyncCore().ConfigureAwait(false);
-            Dispose(false);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
-        /// </summary>
-        /// <returns>A ValueTask.</returns>
-        protected virtual async ValueTask DisposeAsyncCore()
-        {
-            var type = GetType();
-
-            Logger.LogInformation("Disposing {Type}", type.Name);
-
-            foreach (var (part, _) in _disposableParts)
-            {
-                var partType = part.GetType();
-                if (partType == type)
-                {
-                    continue;
-                }
-
-                Logger.LogInformation("Disposing {Type}", partType.Name);
-
-                try
-                {
-                    part.Dispose();
-                }
-                catch (Exception ex)
-                {
-                    Logger.LogError(ex, "Error disposing {Type}", partType.Name);
-                }
-            }
-
-            if (_sessionManager is not null)
-            {
-                // used for closing websockets
-                foreach (var session in _sessionManager.Sessions)
-                {
-                    await session.DisposeAsync().ConfigureAwait(false);
-                }
-            }
-        }
     }
     }
 }
 }

+ 6 - 5
Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
         /// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
         /// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
         /// </summary>
         /// </summary>
         /// <param name="applicationPaths">The application paths.</param>
         /// <param name="applicationPaths">The application paths.</param>
-        /// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param>
+        /// <param name="loggerFactory">The logger factory.</param>
         /// <param name="xmlSerializer">The XML serializer.</param>
         /// <param name="xmlSerializer">The XML serializer.</param>
-        /// <param name="fileSystem">The file system.</param>
-        public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
-            : base(applicationPaths, loggerFactory, xmlSerializer, fileSystem)
+        public ServerConfigurationManager(
+            IApplicationPaths applicationPaths,
+            ILoggerFactory loggerFactory,
+            IXmlSerializer xmlSerializer)
+            : base(applicationPaths, loggerFactory, xmlSerializer)
         {
         {
             UpdateMetadataPath();
             UpdateMetadataPath();
         }
         }

+ 12 - 13
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO
     /// </summary>
     /// </summary>
     public class ManagedFileSystem : IFileSystem
     public class ManagedFileSystem : IFileSystem
     {
     {
-        private readonly ILogger<ManagedFileSystem> _logger;
-
-        private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
-        private readonly string _tempPath;
         private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
         private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
         private static readonly char[] _invalidPathCharacters =
         private static readonly char[] _invalidPathCharacters =
         {
         {
@@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO
             (char)31, ':', '*', '?', '\\', '/'
             (char)31, ':', '*', '?', '\\', '/'
         };
         };
 
 
+        private readonly ILogger<ManagedFileSystem> _logger;
+        private readonly List<IShortcutHandler> _shortcutHandlers;
+        private readonly string _tempPath;
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
         /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
         /// </summary>
         /// </summary>
         /// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
         /// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
         /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
         /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
+        /// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
         public ManagedFileSystem(
         public ManagedFileSystem(
             ILogger<ManagedFileSystem> logger,
             ILogger<ManagedFileSystem> logger,
-            IApplicationPaths applicationPaths)
+            IApplicationPaths applicationPaths,
+            IEnumerable<IShortcutHandler> shortcutHandlers)
         {
         {
             _logger = logger;
             _logger = logger;
             _tempPath = applicationPaths.TempDirectory;
             _tempPath = applicationPaths.TempDirectory;
-        }
-
-        /// <inheritdoc />
-        public virtual void AddShortcutHandler(IShortcutHandler handler)
-        {
-            _shortcutHandlers.Add(handler);
+            _shortcutHandlers = shortcutHandlers.ToList();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -106,15 +103,17 @@ namespace Emby.Server.Implementations.IO
                 return filePath;
                 return filePath;
             }
             }
 
 
+            var filePathSpan = filePath.AsSpan();
+
             // relative path
             // relative path
             if (firstChar == '\\')
             if (firstChar == '\\')
             {
             {
-                filePath = filePath.Substring(1);
+                filePathSpan = filePathSpan.Slice(1);
             }
             }
 
 
             try
             try
             {
             {
-                return Path.GetFullPath(Path.Combine(folderPath, filePath));
+                return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
             }
             }
             catch (ArgumentException)
             catch (ArgumentException)
             {
             {

+ 4 - 0
Emby.Server.Implementations/Library/IgnorePatterns.cs

@@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
             // bts sync files
             // bts sync files
             "**/*.bts",
             "**/*.bts",
             "**/*.sync",
             "**/*.sync",
+
+            // zfs
+            "**/.zfs/**",
+            "**/.zfs"
         };
         };
 
 
         private static readonly GlobOptions _globOptions = new GlobOptions
         private static readonly GlobOptions _globOptions = new GlobOptions

+ 16 - 15
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -46,7 +46,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library
         {
         {
             var path = Person.GetPath(name);
             var path = Person.GetPath(name);
             var id = GetItemByNameId<Person>(path);
             var id = GetItemByNameId<Person>(path);
-            if (GetItemById(id) is not Person item)
+            if (GetItemById(id) is Person item)
             {
             {
-                item = new Person
-                {
-                    Name = name,
-                    Id = id,
-                    DateCreated = DateTime.UtcNow,
-                    DateModified = DateTime.UtcNow,
-                    Path = path
-                };
+                return item;
             }
             }
 
 
-            return item;
+            return null;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1162,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
                 Name = Path.GetFileName(dir),
                 Name = Path.GetFileName(dir),
 
 
                 Locations = _fileSystem.GetFilePaths(dir, false)
                 Locations = _fileSystem.GetFilePaths(dir, false)
-                .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
                     .Select(i =>
                     .Select(i =>
                     {
                     {
                         try
                         try
@@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
                 var saveEntity = false;
                 var saveEntity = false;
                 var personEntity = GetPerson(person.Name);
                 var personEntity = GetPerson(person.Name);
 
 
-                // if PresentationUniqueKey is empty it's likely a new item.
-                if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
+                if (personEntity is null)
                 {
                 {
+                    var path = Person.GetPath(person.Name);
+                    personEntity = new Person()
+                    {
+                        Name = person.Name,
+                        Id = GetItemByNameId<Person>(path),
+                        DateCreated = DateTime.UtcNow,
+                        DateModified = DateTime.UtcNow,
+                        Path = path
+                    };
+
                     personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
                     personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
                     saveEntity = true;
                     saveEntity = true;
                 }
                 }
@@ -3135,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
             }
             }
 
 
             var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
             var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
-                .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
                 .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
                 .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
 
 
             if (!string.IsNullOrEmpty(shortcut))
             if (!string.IsNullOrEmpty(shortcut))

+ 3 - 3
Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
 
             if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
             if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
             {
             {
-                var extension = Path.GetExtension(args.Path);
+                var extension = Path.GetExtension(args.Path.AsSpan());
 
 
-                if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
+                if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     // if audio file exists of same name, return null
                     // if audio file exists of same name, return null
                     return null;
                     return null;
@@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
 
                 if (item is not null)
                 if (item is not null)
                 {
                 {
-                    item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+                    item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
 
 
                     item.IsInMixedFolder = true;
                     item.IsInMixedFolder = true;
                 }
                 }

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 return false;
                 return false;
             }
             }
 
 
-            return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
+            return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 4 - 5
Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs

@@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
                 return GetBook(args);
                 return GetBook(args);
             }
             }
 
 
-            var extension = Path.GetExtension(args.Path);
+            var extension = Path.GetExtension(args.Path.AsSpan());
 
 
-            if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+            if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
             {
                 // It's a book
                 // It's a book
                 return new Book
                 return new Book
@@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
         {
         {
             var bookFiles = args.FileSystemChildren.Where(f =>
             var bookFiles = args.FileSystemChildren.Where(f =>
             {
             {
-                var fileExtension = Path.GetExtension(f.FullName)
-                    ?? string.Empty;
+                var fileExtension = Path.GetExtension(f.FullName.AsSpan());
 
 
                 return _validExtensions.Contains(
                 return _validExtensions.Contains(
                     fileExtension,
                     fileExtension,
-                    StringComparer.OrdinalIgnoreCase);
+                    StringComparison.OrdinalIgnoreCase);
             }).ToList();
             }).ToList();
 
 
             // Don't return a Book if there is more (or less) than one document in the directory
             // Don't return a Book if there is more (or less) than one document in the directory

+ 13 - 14
Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
@@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
         private readonly NamingOptions _namingOptions;
         private readonly NamingOptions _namingOptions;
         private readonly IDirectoryService _directoryService;
         private readonly IDirectoryService _directoryService;
 
 
-        private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly string[] _ignoreFiles = new[]
         {
         {
             "folder",
             "folder",
             "thumb",
             "thumb",
@@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
         /// </summary>
         /// </summary>
         /// <param name="args">The args.</param>
         /// <param name="args">The args.</param>
         /// <returns>Trailer.</returns>
         /// <returns>Trailer.</returns>
-        protected override Photo Resolve(ItemResolveArgs args)
+        protected override Photo? Resolve(ItemResolveArgs args)
         {
         {
             if (!args.IsDirectory)
             if (!args.IsDirectory)
             {
             {
@@ -68,10 +66,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 {
                 {
                     if (IsImageFile(args.Path, _imageProcessor))
                     if (IsImageFile(args.Path, _imageProcessor))
                     {
                     {
-                        var filename = Path.GetFileNameWithoutExtension(args.Path);
+                        var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
 
 
                         // Make sure the image doesn't belong to a video file
                         // Make sure the image doesn't belong to a video file
-                        var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
+                        var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
+                            ?? throw new InvalidOperationException("Path can't be a root directory."));
 
 
                         foreach (var file in files)
                         foreach (var file in files)
                         {
                         {
@@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
             return null;
             return null;
         }
         }
 
 
-        internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
+        internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
         {
         {
             return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
             return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
         }
         }
 
 
-        internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
+        internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
             => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
             => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
 
 
         internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
         internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
         {
         {
             ArgumentNullException.ThrowIfNull(path);
             ArgumentNullException.ThrowIfNull(path);
 
 
-            var filename = Path.GetFileNameWithoutExtension(path);
-
-            if (_ignoreFiles.Contains(filename))
+            var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
+            if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return false;
                 return false;
             }
             }
 
 
-            if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
+            var filename = Path.GetFileNameWithoutExtension(path);
+
+            if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
             {
             {
                 return false;
                 return false;
             }
             }
 
 
-            string extension = Path.GetExtension(path).TrimStart('.');
-            return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
+            return true;
         }
         }
     }
     }
 }
 }

+ 1 - 8
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
                 else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
                 {
                 {
                     var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
                     var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
-                    if (string.IsNullOrWhiteSpace(channel.Id))
-                    {
-                        channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-                    }
-                    else
-                    {
-                        channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-                    }
+                    channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 
 
                     channel.Path = trimmedLine;
                     channel.Path = trimmedLine;
                     channels.Add(channel);
                     channels.Add(channel);

+ 43 - 1
Emby.Server.Implementations/Localization/Core/as.json

@@ -1 +1,43 @@
-{}
+{
+    "Albums": "এলবাম",
+    "Application": "আবেদন",
+    "AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}",
+    "Artists": "শিল্পী",
+    "Channels": "চেনেলস",
+    "Default": "ডিফল্ট",
+    "AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
+    "Books": "পুস্তক",
+    "Movies": "চলচ্চিত্ৰ",
+    "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
+    "Collections": "সংগ্রহ",
+    "HeaderFavoriteShows": "প্রিয় শোসমূহ",
+    "Latest": "শেহতীয়া",
+    "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
+    "MixedContent": "মিশ্ৰিত সমগ্ৰতা",
+    "NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
+    "NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
+    "External": "বাহ্যিক",
+    "Favorites": "পছন্দসই",
+    "Folders": "ফোল্ডাৰ",
+    "Forced": "বলপূর্বক",
+    "Genres": "শ্রেণী",
+    "HeaderAlbumArtists": "অ্যালবাম শিল্পী",
+    "HeaderContinueWatching": "দেখা চালিয়ে যান",
+    "FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
+    "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
+    "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
+    "HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
+    "HeaderFavoriteSongs": "প্ৰিয় গীত",
+    "HeaderLiveTV": "প্ৰতিবেদন টিভি",
+    "HeaderNextUp": "পৰৱৰ্তী অংশ",
+    "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
+    "HearingImpaired": "শ্ৰবণ অক্ষম",
+    "HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
+    "Inherit": "উত্তপ্ত কৰা",
+    "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
+    "NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
+    "NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
+    "NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",
+    "NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল",
+    "NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা"
+}

+ 52 - 0
Emby.Server.Implementations/Localization/Core/chr.json

@@ -0,0 +1,52 @@
+{
+    "ChapterNameValue": "Didanedi {0}",
+    "HeaderAlbumArtists": "Didanidanolisgisgi",
+    "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
+    "HeaderLiveTV": "Anigadi didanidisgosgi",
+    "HeaderRecordingGroups": "Didanisquodiisgisgi",
+    "HomeVideos": "Diganadi dinagadisgisgi",
+    "Inherit": "Anigwe",
+    "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
+    "MixedContent": "Ganinidi dininoladisgisgi",
+    "Movies": "Anidvnisgisgi",
+    "MusicVideos": "Danodisgisgi didanidisgosgi",
+    "NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
+    "NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
+    "NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
+    "Albums": "Anigawidaniyv",
+    "Application": "Didanvyi",
+    "Artists": "Dinidaniyi",
+    "AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
+    "Books": "Didanedi",
+    "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
+    "Channels": "Diganadasgi",
+    "Collections": "Diganadisgi",
+    "Default": "Dinadi",
+    "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
+    "External": "Amohdi",
+    "Favorites": "Nvdayelvdisgi",
+    "Folders": "Didanididisgi",
+    "Forced": "Ganedi",
+    "Genres": "Diganadisgi",
+    "HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
+    "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
+    "HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
+    "HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
+    "HeaderFavoriteSongs": "Dvganidi danodisgisgi",
+    "HeaderNextUp": "Anidvli uwodoli",
+    "HearingImpaired": "Anitsunidi talunidisgisgi",
+    "ItemAddedWithName": "{0} Dinigwe anididanidisgi",
+    "Latest": "Uwodoli",
+    "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
+    "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
+    "Music": "Danodisgisgi",
+    "NameSeasonUnknown": "Tsunita anidvdisgi",
+    "NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",
+    "NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi",
+    "NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi",
+    "NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi",
+    "NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi",
+    "NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi",
+    "NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi",
+    "NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi"
+}

+ 24 - 24
Emby.Server.Implementations/Localization/Core/da.json

@@ -15,13 +15,13 @@
     "Favorites": "Favoritter",
     "Favorites": "Favoritter",
     "Folders": "Mapper",
     "Folders": "Mapper",
     "Genres": "Genrer",
     "Genres": "Genrer",
-    "HeaderAlbumArtists": "Albums kunstnere",
+    "HeaderAlbumArtists": "Albumkunstnere",
     "HeaderContinueWatching": "Fortsæt afspilning",
     "HeaderContinueWatching": "Fortsæt afspilning",
-    "HeaderFavoriteAlbums": "Favorit albummer",
-    "HeaderFavoriteArtists": "Favorit kunstnere",
-    "HeaderFavoriteEpisodes": "Favorit afsnit",
-    "HeaderFavoriteShows": "Favorit serier",
-    "HeaderFavoriteSongs": "Favorit sange",
+    "HeaderFavoriteAlbums": "Favoritalbummer",
+    "HeaderFavoriteArtists": "Favoritkunstnere",
+    "HeaderFavoriteEpisodes": "Yndlingsafsnit",
+    "HeaderFavoriteShows": "Yndlingsserier",
+    "HeaderFavoriteSongs": "Yndlingssange",
     "HeaderLiveTV": "Live-TV",
     "HeaderLiveTV": "Live-TV",
     "HeaderNextUp": "Næste",
     "HeaderNextUp": "Næste",
     "HeaderRecordingGroups": "Optagelsesgrupper",
     "HeaderRecordingGroups": "Optagelsesgrupper",
@@ -34,8 +34,8 @@
     "Latest": "Seneste",
     "Latest": "Seneste",
     "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
     "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
     "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
     "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
-    "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
+    "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
     "MixedContent": "Blandet indhold",
     "MixedContent": "Blandet indhold",
     "Movies": "Film",
     "Movies": "Film",
     "Music": "Musik",
     "Music": "Musik",
@@ -51,7 +51,7 @@
     "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
     "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
     "NotificationOptionInstallationFailed": "Installationen mislykkedes",
     "NotificationOptionInstallationFailed": "Installationen mislykkedes",
     "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
     "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
-    "NotificationOptionPluginError": "Plugin fejl",
+    "NotificationOptionPluginError": "Plugin-fejl",
     "NotificationOptionPluginInstalled": "Plugin blev installeret",
     "NotificationOptionPluginInstalled": "Plugin blev installeret",
     "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
     "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
     "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
     "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
@@ -92,26 +92,26 @@
     "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
     "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
     "ValueSpecialEpisodeName": "Special - {0}",
     "ValueSpecialEpisodeName": "Special - {0}",
     "VersionNumber": "Version {0}",
     "VersionNumber": "Version {0}",
-    "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
+    "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
     "TaskDownloadMissingSubtitles": "Hent manglende undertekster",
     "TaskDownloadMissingSubtitles": "Hent manglende undertekster",
     "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
     "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
     "TaskUpdatePlugins": "Opdater Plugins",
     "TaskUpdatePlugins": "Opdater Plugins",
-    "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
-    "TaskCleanLogs": "Ryd Log mappe",
-    "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
-    "TaskRefreshLibrary": "Scan Medie Bibliotek",
-    "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
-    "TaskCleanCache": "Ryd Cache mappe",
-    "TasksChannelsCategory": "Internet Kanaler",
+    "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
+    "TaskCleanLogs": "Ryd Log-mappe",
+    "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
+    "TaskRefreshLibrary": "Scan Mediebibliotek",
+    "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
+    "TaskCleanCache": "Ryd Cache-mappe",
+    "TasksChannelsCategory": "Internetkanaler",
     "TasksApplicationCategory": "Applikation",
     "TasksApplicationCategory": "Applikation",
     "TasksLibraryCategory": "Bibliotek",
     "TasksLibraryCategory": "Bibliotek",
     "TasksMaintenanceCategory": "Vedligeholdelse",
     "TasksMaintenanceCategory": "Vedligeholdelse",
-    "TaskRefreshChapterImages": "Udtræk kapitel billeder",
-    "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
-    "TaskRefreshChannelsDescription": "Opdater internet kanal information.",
+    "TaskRefreshChapterImages": "Udtræk kapitelbilleder",
+    "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
+    "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
     "TaskRefreshChannels": "Opdater Kanaler",
     "TaskRefreshChannels": "Opdater Kanaler",
-    "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
-    "TaskCleanTranscode": "Tøm Transcode mappen",
+    "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
+    "TaskCleanTranscode": "Tøm Transcode-mappen",
     "TaskRefreshPeople": "Opdater Personer",
     "TaskRefreshPeople": "Opdater Personer",
     "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
     "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
     "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
     "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
@@ -121,8 +121,8 @@
     "Default": "Standard",
     "Default": "Standard",
     "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
     "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
     "TaskOptimizeDatabase": "Optimér database",
     "TaskOptimizeDatabase": "Optimér database",
-    "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
-    "TaskKeyframeExtractor": "Nøglebillede udtræk",
+    "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
+    "TaskKeyframeExtractor": "Udtræk af nøglebillede",
     "External": "Ekstern",
     "External": "Ekstern",
     "HearingImpaired": "Hørehæmmet"
     "HearingImpaired": "Hørehæmmet"
 }
 }

+ 2 - 2
Emby.Server.Implementations/Localization/Core/es.json

@@ -3,9 +3,9 @@
     "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
     "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
     "Application": "Aplicación",
     "Application": "Aplicación",
     "Artists": "Artistas",
     "Artists": "Artistas",
-    "AuthenticationSucceededWithUserName": "{0} identificado correctamente",
+    "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
     "Books": "Libros",
     "Books": "Libros",
-    "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
+    "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
     "Channels": "Canales",
     "Channels": "Canales",
     "ChapterNameValue": "Capítulo {0}",
     "ChapterNameValue": "Capítulo {0}",
     "Collections": "Colecciones",
     "Collections": "Colecciones",

+ 2 - 2
Emby.Server.Implementations/Localization/Core/fr.json

@@ -105,8 +105,8 @@
     "TaskRefreshPeople": "Actualiser les acteurs",
     "TaskRefreshPeople": "Actualiser les acteurs",
     "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
     "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
     "TaskCleanLogs": "Nettoyer le répertoire des journaux",
     "TaskCleanLogs": "Nettoyer le répertoire des journaux",
-    "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
-    "TaskRefreshLibrary": "Scanner la médiathèque",
+    "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
+    "TaskRefreshLibrary": "Analyser la médiathèque",
     "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
     "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
     "TaskRefreshChapterImages": "Extraire les images de chapitre",
     "TaskRefreshChapterImages": "Extraire les images de chapitre",
     "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
     "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",

+ 121 - 1
Emby.Server.Implementations/Localization/Core/kn.json

@@ -3,5 +3,125 @@
     "TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
     "TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
     "TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
     "TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
     "TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್",
     "TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್",
-    "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು."
+    "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
+    "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
+    "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
+    "TasksLibraryCategory": "ಸಮೊಹ",
+    "TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
+    "TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
+    "TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
+    "TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+    "TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
+    "UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+    "Albums": "ಸಂಪುಟ",
+    "Application": "ಅಪ್ಲಿಕೇಶನ್",
+    "AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
+    "Artists": "ಕಲಾವಿದರು",
+    "AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
+    "Books": "ಪುಸ್ತಕಗಳು",
+    "ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
+    "Collections": "ಸಂಗ್ರಹಣೆಗಳು",
+    "Default": "ಪೂರ್ವನಿಯೋಜಿತ",
+    "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+    "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
+    "External": "ಹೊರಗಿನ",
+    "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
+    "Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
+    "Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
+    "Forced": "ಬಲವಂತವಾಗಿ",
+    "Genres": "ಪ್ರಕಾರಗಳು",
+    "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
+    "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
+    "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
+    "HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
+    "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
+    "HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
+    "HeaderNextUp": "ಮುಂದೆ",
+    "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
+    "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+    "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+    "Channels": "ಮೂಲಗಳು",
+    "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
+    "HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
+    "HearingImpaired": "ಮೂಗ",
+    "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
+    "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+    "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+    "NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.",
+    "NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+    "NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+    "NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+    "NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
+    "NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+    "PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+    "ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
+    "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
+    "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
+    "UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
+    "UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
+    "UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ",
+    "UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
+    "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+    "UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
+    "UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
+    "VersionNumber": "ಆವೃತ್ತಿ {0}",
+    "TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
+    "TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
+    "TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+    "TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
+    "TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್‌ನೇಲ್‌ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
+    "TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್‌ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
+    "TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+    "TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್‌ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
+    "TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+    "TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ",
+    "Shows": "ಧಾರವಾಹಿಗಳು",
+    "Songs": "ಹಾಡುಗಳು",
+    "StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
+    "UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
+    "UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
+    "SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
+    "Sync": "ಹೊಂದಿಕೆ",
+    "System": "ವ್ಯವಸ್ಥೆ",
+    "TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
+    "Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
+    "User": "ಬಳಕೆದಾರ",
+    "HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
+    "Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
+    "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
+    "LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
+    "LabelRunningTimeValue": "ಅವಧಿ: {0}",
+    "Latest": "ಹೊಸದಾದ",
+    "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+    "MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
+    "Movies": "ಚಲನಚಿತ್ರಗಳು",
+    "Music": "ಸಂಗೀತ",
+    "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
+    "NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
+    "NameSeasonNumber": "ಸೀಸನ್ {0}",
+    "NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
+    "NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
+    "NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+    "NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+    "NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
+    "NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
+    "NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+    "NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+    "NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+    "NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
+    "NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
+    "NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+    "Photos": "ಚಿತ್ರಗಳು",
+    "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
+    "Plugin": "ಪ್ಲಗಿನ್",
+    "PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+    "PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+    "ProviderValue": "ಒದಗಿಸುವವರು: {0}",
+    "TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
+    "TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+    "TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
+    "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
+    "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
+    "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+    "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
 }
 }

+ 3 - 1
Emby.Server.Implementations/Localization/Core/ml.json

@@ -121,5 +121,7 @@
     "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
     "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
     "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
     "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
     "HearingImpaired": "കേൾവി തകരാറുകൾ",
     "HearingImpaired": "കേൾവി തകരാറുകൾ",
-    "External": "പുറമേയുള്ള"
+    "External": "പുറമേയുള്ള",
+    "TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
+    "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
 }
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ms.json

@@ -1,5 +1,5 @@
 {
 {
-    "Albums": "Albums",
+    "Albums": "Album",
     "AppDeviceValues": "Apl: {0}, Peranti: {1}",
     "AppDeviceValues": "Apl: {0}, Peranti: {1}",
     "Application": "Aplikasi",
     "Application": "Aplikasi",
     "Artists": "Artis-artis",
     "Artists": "Artis-artis",

+ 4 - 1
Emby.Server.Implementations/Localization/Core/pr.json

@@ -29,5 +29,8 @@
     "Forced": "Pressed",
     "Forced": "Pressed",
     "External": "Outboard",
     "External": "Outboard",
     "HeaderFavoriteEpisodes": "Treasured Tales",
     "HeaderFavoriteEpisodes": "Treasured Tales",
-    "HeaderFavoriteShows": "Treasured Tales"
+    "HeaderFavoriteShows": "Treasured Tales",
+    "ChapterNameValue": "Piece {0}",
+    "HeaderFavoriteSongs": "Treasured Chimes",
+    "HeaderNextUp": "Incoming"
 }
 }

+ 10 - 1
Emby.Server.Implementations/Localization/Core/zu.json

@@ -25,5 +25,14 @@
     "Channels": "Amashaneli",
     "Channels": "Amashaneli",
     "Books": "Izincwadi",
     "Books": "Izincwadi",
     "Artists": "Abadlali",
     "Artists": "Abadlali",
-    "Albums": "Ama-albhamu"
+    "Albums": "Ama-albhamu",
+    "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
+    "HeaderFavoriteArtists": "Abasethi Abathandekayo",
+    "HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
+    "HeaderFavoriteShows": "Izisho Ezithandekayo",
+    "External": "Kwezifungo",
+    "FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
+    "HeaderContinueWatching": "Buyela Ukubona",
+    "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
+    "HeaderAlbumArtists": "Abasethi wenkulumo"
 }
 }

+ 1 - 1
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
         {
         {
             var deadImages = images
             var deadImages = images
                 .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
                 .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
-                .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+                .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
                 .ToList();
                 .ToList();
 
 
             foreach (var image in deadImages)
             foreach (var image in deadImages)

+ 6 - 10
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists
             // this is probably best done as a metadata provider
             // this is probably best done as a metadata provider
             // saving a file over itself will require some work to prevent this from happening when not needed
             // saving a file over itself will require some work to prevent this from happening when not needed
             var playlistPath = item.Path;
             var playlistPath = item.Path;
-            var extension = Path.GetExtension(playlistPath);
+            var extension = Path.GetExtension(playlistPath.AsSpan());
 
 
-            if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
+            if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new WplPlaylist();
                 var playlist = new WplPlaylist();
                 foreach (var child in item.GetLinkedChildren())
                 foreach (var child in item.GetLinkedChildren())
@@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists
                 string text = new WplContent().ToText(playlist);
                 string text = new WplContent().ToText(playlist);
                 File.WriteAllText(playlistPath, text);
                 File.WriteAllText(playlistPath, text);
             }
             }
-
-            if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
+            else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new ZplPlaylist();
                 var playlist = new ZplPlaylist();
                 foreach (var child in item.GetLinkedChildren())
                 foreach (var child in item.GetLinkedChildren())
@@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists
                 string text = new ZplContent().ToText(playlist);
                 string text = new ZplContent().ToText(playlist);
                 File.WriteAllText(playlistPath, text);
                 File.WriteAllText(playlistPath, text);
             }
             }
-
-            if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
+            else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new M3uPlaylist
                 var playlist = new M3uPlaylist
                 {
                 {
@@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists
                 string text = new M3uContent().ToText(playlist);
                 string text = new M3uContent().ToText(playlist);
                 File.WriteAllText(playlistPath, text);
                 File.WriteAllText(playlistPath, text);
             }
             }
-
-            if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
+            else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new M3uPlaylist();
                 var playlist = new M3uPlaylist();
                 playlist.IsExtended = true;
                 playlist.IsExtended = true;
@@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists
                 string text = new M3uContent().ToText(playlist);
                 string text = new M3uContent().ToText(playlist);
                 File.WriteAllText(playlistPath, text);
                 File.WriteAllText(playlistPath, text);
             }
             }
-
-            if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
+            else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new PlsPlaylist();
                 var playlist = new PlsPlaylist();
                 foreach (var child in item.GetLinkedChildren())
                 foreach (var child in item.GetLinkedChildren())

+ 10 - 12
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -1,6 +1,5 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Data;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
@@ -11,7 +10,6 @@ using System.Text;
 using System.Text.Json;
 using System.Text.Json;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.Library;
-using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
 using Jellyfin.Extensions.Json;
 using Jellyfin.Extensions.Json.Converters;
 using Jellyfin.Extensions.Json.Converters;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
@@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins
     /// <summary>
     /// <summary>
     /// Defines the <see cref="PluginManager" />.
     /// Defines the <see cref="PluginManager" />.
     /// </summary>
     /// </summary>
-    public class PluginManager : IPluginManager
+    public sealed class PluginManager : IPluginManager, IDisposable
     {
     {
         private const string MetafileName = "meta.json";
         private const string MetafileName = "meta.json";
 
 
@@ -191,15 +189,6 @@ namespace Emby.Server.Implementations.Plugins
             }
             }
         }
         }
 
 
-        /// <inheritdoc />
-        public void UnloadAssemblies()
-        {
-            foreach (var assemblyLoadContext in _assemblyLoadContexts)
-            {
-                assemblyLoadContext.Unload();
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Creates all the plugin instances.
         /// Creates all the plugin instances.
         /// </summary>
         /// </summary>
@@ -441,6 +430,15 @@ namespace Emby.Server.Implementations.Plugins
             return SaveManifest(manifest, path);
             return SaveManifest(manifest, path);
         }
         }
 
 
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            foreach (var assemblyLoadContext in _assemblyLoadContexts)
+            {
+                assemblyLoadContext.Unload();
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
         /// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
         /// If no file is found, no reconciliation occurs.
         /// If no file is found, no reconciliation occurs.

+ 74 - 81
Emby.Server.Implementations/Session/SessionManager.cs

@@ -36,6 +36,7 @@ using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.SyncPlay;
 using MediaBrowser.Model.SyncPlay;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 
 
@@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session
     /// <summary>
     /// <summary>
     /// Class SessionManager.
     /// Class SessionManager.
     /// </summary>
     /// </summary>
-    public class SessionManager : ISessionManager, IDisposable
+    public sealed class SessionManager : ISessionManager, IAsyncDisposable
     {
     {
         private readonly IUserDataManager _userDataManager;
         private readonly IUserDataManager _userDataManager;
         private readonly ILogger<SessionManager> _logger;
         private readonly ILogger<SessionManager> _logger;
@@ -57,11 +58,9 @@ namespace Emby.Server.Implementations.Session
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
         private readonly IDeviceManager _deviceManager;
         private readonly IDeviceManager _deviceManager;
-
-        /// <summary>
-        /// The active connections.
-        /// </summary>
-        private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase);
+        private readonly CancellationTokenRegistration _shutdownCallback;
+        private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
+            = new(StringComparer.OrdinalIgnoreCase);
 
 
         private Timer _idleTimer;
         private Timer _idleTimer;
 
 
@@ -79,7 +78,8 @@ namespace Emby.Server.Implementations.Session
             IImageProcessor imageProcessor,
             IImageProcessor imageProcessor,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             IDeviceManager deviceManager,
             IDeviceManager deviceManager,
-            IMediaSourceManager mediaSourceManager)
+            IMediaSourceManager mediaSourceManager,
+            IHostApplicationLifetime hostApplicationLifetime)
         {
         {
             _logger = logger;
             _logger = logger;
             _eventManager = eventManager;
             _eventManager = eventManager;
@@ -92,6 +92,7 @@ namespace Emby.Server.Implementations.Session
             _appHost = appHost;
             _appHost = appHost;
             _deviceManager = deviceManager;
             _deviceManager = deviceManager;
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
+            _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
 
 
             _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
             _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
         }
         }
@@ -151,36 +152,6 @@ namespace Emby.Server.Implementations.Session
             }
             }
         }
         }
 
 
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and optionally managed resources.
-        /// </summary>
-        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                _idleTimer?.Dispose();
-            }
-
-            _idleTimer = null;
-
-            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
-
-            _disposed = true;
-        }
-
         private void CheckDisposed()
         private void CheckDisposed()
         {
         {
             if (_disposed)
             if (_disposed)
@@ -980,28 +951,28 @@ namespace Emby.Server.Implementations.Session
 
 
         private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
         private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
         {
         {
-            bool playedToCompletion = false;
-
-            if (!playbackFailed)
+            if (playbackFailed)
             {
             {
-                var data = _userDataManager.GetUserData(user, item);
-
-                if (positionTicks.HasValue)
-                {
-                    playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
-                }
-                else
-                {
-                    // If the client isn't able to report this, then we'll just have to make an assumption
-                    data.PlayCount++;
-                    data.Played = item.SupportsPlayedStatus;
-                    data.PlaybackPositionTicks = 0;
-                    playedToCompletion = true;
-                }
+                return false;
+            }
 
 
-                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
+            var data = _userDataManager.GetUserData(user, item);
+            bool playedToCompletion;
+            if (positionTicks.HasValue)
+            {
+                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
+            }
+            else
+            {
+                // If the client isn't able to report this, then we'll just have to make an assumption
+                data.PlayCount++;
+                data.Played = item.SupportsPlayedStatus;
+                data.PlaybackPositionTicks = 0;
+                playedToCompletion = true;
             }
             }
 
 
+            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
+
             return playedToCompletion;
             return playedToCompletion;
         }
         }
 
 
@@ -1330,32 +1301,6 @@ namespace Emby.Server.Implementations.Session
             return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
             return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
         }
         }
 
 
-        /// <summary>
-        /// Sends the server shutdown notification.
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendServerShutdownNotification(CancellationToken cancellationToken)
-        {
-            CheckDisposed();
-
-            return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
-        }
-
-        /// <summary>
-        /// Sends the server restart notification.
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendServerRestartNotification(CancellationToken cancellationToken)
-        {
-            CheckDisposed();
-
-            _logger.LogDebug("Beginning SendServerRestartNotification");
-
-            return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
-        }
-
         /// <summary>
         /// <summary>
         /// Adds the additional user.
         /// Adds the additional user.
         /// </summary>
         /// </summary>
@@ -1833,5 +1778,53 @@ namespace Emby.Server.Implementations.Session
 
 
             return SendMessageToSessions(sessions, name, data, cancellationToken);
             return SendMessageToSessions(sessions, name, data, cancellationToken);
         }
         }
+
+        /// <inheritdoc />
+        public async ValueTask DisposeAsync()
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            foreach (var session in _activeConnections.Values)
+            {
+                await session.DisposeAsync().ConfigureAwait(false);
+            }
+
+            if (_idleTimer is not null)
+            {
+                await _idleTimer.DisposeAsync().ConfigureAwait(false);
+                _idleTimer = null;
+            }
+
+            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
+
+            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
+            _disposed = true;
+        }
+
+        private async void OnApplicationStopping()
+        {
+            _logger.LogInformation("Sending shutdown notifications");
+            try
+            {
+                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown;
+
+                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error sending server shutdown notifications");
+            }
+
+            // Close open websockets to allow Kestrel to shut down cleanly
+            foreach (var session in _activeConnections.Values)
+            {
+                await session.DisposeAsync().ConfigureAwait(false);
+            }
+
+            _activeConnections.Clear();
+        }
     }
     }
 }
 }

+ 109 - 0
Emby.Server.Implementations/SystemManager.cs

@@ -0,0 +1,109 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Hosting;
+
+namespace Emby.Server.Implementations;
+
+/// <inheritdoc />
+public class SystemManager : ISystemManager
+{
+    private readonly IHostApplicationLifetime _applicationLifetime;
+    private readonly IServerApplicationHost _applicationHost;
+    private readonly IServerApplicationPaths _applicationPaths;
+    private readonly IServerConfigurationManager _configurationManager;
+    private readonly IStartupOptions _startupOptions;
+    private readonly IInstallationManager _installationManager;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="SystemManager"/> class.
+    /// </summary>
+    /// <param name="applicationLifetime">Instance of <see cref="IHostApplicationLifetime"/>.</param>
+    /// <param name="applicationHost">Instance of <see cref="IServerApplicationHost"/>.</param>
+    /// <param name="applicationPaths">Instance of <see cref="IServerApplicationPaths"/>.</param>
+    /// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param>
+    /// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param>
+    public SystemManager(
+        IHostApplicationLifetime applicationLifetime,
+        IServerApplicationHost applicationHost,
+        IServerApplicationPaths applicationPaths,
+        IServerConfigurationManager configurationManager,
+        IStartupOptions startupOptions,
+        IInstallationManager installationManager)
+    {
+        _applicationLifetime = applicationLifetime;
+        _applicationHost = applicationHost;
+        _applicationPaths = applicationPaths;
+        _configurationManager = configurationManager;
+        _startupOptions = startupOptions;
+        _installationManager = installationManager;
+    }
+
+    private bool CanLaunchWebBrowser => Environment.UserInteractive
+        && !_startupOptions.IsService
+        && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
+
+    /// <inheritdoc />
+    public SystemInfo GetSystemInfo(HttpRequest request)
+    {
+        return new SystemInfo
+        {
+            HasPendingRestart = _applicationHost.HasPendingRestart,
+            IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
+            Version = _applicationHost.ApplicationVersionString,
+            WebSocketPortNumber = _applicationHost.HttpPort,
+            CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
+            Id = _applicationHost.SystemId,
+            ProgramDataPath = _applicationPaths.ProgramDataPath,
+            WebPath = _applicationPaths.WebPath,
+            LogPath = _applicationPaths.LogDirectoryPath,
+            ItemsByNamePath = _applicationPaths.InternalMetadataPath,
+            InternalMetadataPath = _applicationPaths.InternalMetadataPath,
+            CachePath = _applicationPaths.CachePath,
+            CanLaunchWebBrowser = CanLaunchWebBrowser,
+            TranscodingTempPath = _configurationManager.GetTranscodePath(),
+            ServerName = _applicationHost.FriendlyName,
+            LocalAddress = _applicationHost.GetSmartApiUrl(request),
+            SupportsLibraryMonitor = true,
+            PackageName = _startupOptions.PackageName,
+            CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
+        };
+    }
+
+    /// <inheritdoc />
+    public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
+    {
+        return new PublicSystemInfo
+        {
+            Version = _applicationHost.ApplicationVersionString,
+            ProductName = _applicationHost.Name,
+            Id = _applicationHost.SystemId,
+            ServerName = _applicationHost.FriendlyName,
+            LocalAddress = _applicationHost.GetSmartApiUrl(request),
+            StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted
+        };
+    }
+
+    /// <inheritdoc />
+    public void Restart() => ShutdownInternal(true);
+
+    /// <inheritdoc />
+    public void Shutdown() => ShutdownInternal(false);
+
+    private void ShutdownInternal(bool restart)
+    {
+        Task.Run(async () =>
+        {
+            await Task.Delay(100).ConfigureAwait(false);
+            _applicationHost.ShouldRestart = restart;
+            _applicationLifetime.StopApplication();
+        });
+    }
+}

+ 1 - 2
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
 
 
         private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
         private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
         {
         {
-            var extension = Path.GetExtension(package.SourceUrl);
-            if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
+            if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
             {
             {
                 _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
                 _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
                 return;
                 return;

+ 23 - 18
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
     private const string DefaultEventEncoderPreset = "superfast";
     private const string DefaultEventEncoderPreset = "superfast";
     private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
     private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
 
 
+    private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
+
     private readonly ILibraryManager _libraryManager;
     private readonly ILibraryManager _libraryManager;
     private readonly IUserManager _userManager;
     private readonly IUserManager _userManager;
     private readonly IDlnaManager _dlnaManager;
     private readonly IDlnaManager _dlnaManager;
@@ -1705,16 +1707,31 @@ public class DynamicHlsController : BaseJellyfinApiController
         var audioCodec = _encodingHelper.GetAudioEncoder(state);
         var audioCodec = _encodingHelper.GetAudioEncoder(state);
         var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
         var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
 
 
+        // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
+        var strictArgs = string.Empty;
+        var actualOutputAudioCodec = state.ActualOutputAudioCodec;
+        if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+            || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+            || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
+            || (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+                && _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
+        {
+            strictArgs = " -strict -2";
+        }
+
         if (!state.IsOutputVideo)
         if (!state.IsOutputVideo)
         {
         {
+            var audioTranscodeParams = string.Empty;
+
+            // -vn to drop any video streams
+            audioTranscodeParams += "-vn";
+
             if (EncodingHelper.IsCopyCodec(audioCodec))
             if (EncodingHelper.IsCopyCodec(audioCodec))
             {
             {
-                return "-acodec copy -strict -2" + bitStreamArgs;
+                return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
             }
             }
 
 
-            var audioTranscodeParams = string.Empty;
-
-            audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs;
+            audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
 
 
             var audioBitrate = state.OutputAudioBitrate;
             var audioBitrate = state.OutputAudioBitrate;
             var audioChannels = state.OutputAudioChannels;
             var audioChannels = state.OutputAudioChannels;
@@ -1742,21 +1759,9 @@ public class DynamicHlsController : BaseJellyfinApiController
                 audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
                 audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
             }
             }
 
 
-            audioTranscodeParams += " -vn";
             return audioTranscodeParams;
             return audioTranscodeParams;
         }
         }
 
 
-        // dts, flac, opus and truehd are experimental in mp4 muxer
-        var strictArgs = string.Empty;
-        var actualOutputAudioCodec = state.ActualOutputAudioCodec;
-        if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
-            || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
-            || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
-            || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
-        {
-            strictArgs = " -strict -2";
-        }
-
         if (EncodingHelper.IsCopyCodec(audioCodec))
         if (EncodingHelper.IsCopyCodec(audioCodec))
         {
         {
             var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
             var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
@@ -2041,9 +2046,9 @@ public class DynamicHlsController : BaseJellyfinApiController
             return null;
             return null;
         }
         }
 
 
-        var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+        var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
 
 
-        var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+        var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
 
 
         return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
         return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
     }
     }

+ 5 - 4
Jellyfin.Api/Controllers/HlsSegmentController.cs

@@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
     public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
     public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
     {
     {
         // TODO: Deprecate with new iOS app
         // TODO: Deprecate with new iOS app
-        var file = segmentId + Path.GetExtension(Request.Path);
+        var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
         var transcodePath = _serverConfigurationManager.GetTranscodePath();
         var transcodePath = _serverConfigurationManager.GetTranscodePath();
         file = Path.GetFullPath(Path.Combine(transcodePath, file));
         file = Path.GetFullPath(Path.Combine(transcodePath, file));
         var fileDir = Path.GetDirectoryName(file);
         var fileDir = Path.GetDirectoryName(file);
@@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
     public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
     public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
     {
     {
-        var file = playlistId + Path.GetExtension(Request.Path);
+        var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
         var transcodePath = _serverConfigurationManager.GetTranscodePath();
         var transcodePath = _serverConfigurationManager.GetTranscodePath();
         file = Path.GetFullPath(Path.Combine(transcodePath, file));
         file = Path.GetFullPath(Path.Combine(transcodePath, file));
         var fileDir = Path.GetDirectoryName(file);
         var fileDir = Path.GetDirectoryName(file);
-        if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
+        if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
+            || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
         {
         {
             return BadRequest("Invalid segment.");
             return BadRequest("Invalid segment.");
         }
         }
@@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
         [FromRoute, Required] string segmentId,
         [FromRoute, Required] string segmentId,
         [FromRoute, Required] string segmentContainer)
         [FromRoute, Required] string segmentContainer)
     {
     {
-        var file = segmentId + Path.GetExtension(Request.Path);
+        var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
         var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
         var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
 
 
         file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
         file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));

+ 19 - 24
Jellyfin.Api/Controllers/ImageController.cs

@@ -7,6 +7,7 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net.Mime;
 using System.Net.Mime;
+using System.Security.Cryptography;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
@@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
         _appPaths = appPaths;
         _appPaths = appPaths;
     }
     }
 
 
+    private static Stream GetFromBase64Stream(Stream inputStream)
+        => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
+
     /// <summary>
     /// <summary>
     /// Sets the user image.
     /// Sets the user image.
     /// </summary>
     /// </summary>
@@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             // Handle image/png; charset=utf-8
             // Handle image/png; charset=utf-8
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
 
 
             await _providerManager
             await _providerManager
-                .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+                .SaveImage(stream, mimeType, user.ProfileImage.Path)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
             await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
             await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 
 
@@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             // Handle image/png; charset=utf-8
             // Handle image/png; charset=utf-8
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
 
 
             await _providerManager
             await _providerManager
-                .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+                .SaveImage(stream, mimeType, user.ProfileImage.Path)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
             await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
             await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 
 
@@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             // Handle image/png; charset=utf-8
             // Handle image/png; charset=utf-8
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
-            await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
             await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
             await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 
 
             return NoContent();
             return NoContent();
@@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             // Handle image/png; charset=utf-8
             // Handle image/png; charset=utf-8
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
-            await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
             await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
             await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 
 
             return NoContent();
             return NoContent();
@@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
             var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
             var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
             var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
@@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
             var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
             var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
             await using (fs.ConfigureAwait(false))
             await using (fs.ConfigureAwait(false))
             {
             {
-                await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
+                await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
             }
             }
 
 
             return NoContent();
             return NoContent();
@@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
         return NoContent();
         return NoContent();
     }
     }
 
 
-    private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
-    {
-        using var reader = new StreamReader(inputStream);
-        var text = await reader.ReadToEndAsync().ConfigureAwait(false);
-
-        var bytes = Convert.FromBase64String(text);
-        return new MemoryStream(bytes, 0, bytes.Length, false, true);
-    }
-
     private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
     private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
     {
     {
         int? width = null;
         int? width = null;

+ 1 - 6
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
@@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController
     private readonly IMediaSourceManager _mediaSourceManager;
     private readonly IMediaSourceManager _mediaSourceManager;
     private readonly IConfigurationManager _configurationManager;
     private readonly IConfigurationManager _configurationManager;
     private readonly TranscodingJobHelper _transcodingJobHelper;
     private readonly TranscodingJobHelper _transcodingJobHelper;
-    private readonly ISessionManager _sessionManager;
 
 
     /// <summary>
     /// <summary>
     /// Initializes a new instance of the <see cref="LiveTvController"/> class.
     /// Initializes a new instance of the <see cref="LiveTvController"/> class.
@@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController
     /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
     /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
     /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
     /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
     /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
     /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
-    /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
     public LiveTvController(
     public LiveTvController(
         ILiveTvManager liveTvManager,
         ILiveTvManager liveTvManager,
         IUserManager userManager,
         IUserManager userManager,
@@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController
         IDtoService dtoService,
         IDtoService dtoService,
         IMediaSourceManager mediaSourceManager,
         IMediaSourceManager mediaSourceManager,
         IConfigurationManager configurationManager,
         IConfigurationManager configurationManager,
-        TranscodingJobHelper transcodingJobHelper,
-        ISessionManager sessionManager)
+        TranscodingJobHelper transcodingJobHelper)
     {
     {
         _liveTvManager = liveTvManager;
         _liveTvManager = liveTvManager;
         _userManager = userManager;
         _userManager = userManager;
@@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController
         _mediaSourceManager = mediaSourceManager;
         _mediaSourceManager = mediaSourceManager;
         _configurationManager = configurationManager;
         _configurationManager = configurationManager;
         _transcodingJobHelper = transcodingJobHelper;
         _transcodingJobHelper = transcodingJobHelper;
-        _sessionManager = sessionManager;
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 4 - 4
Jellyfin.Api/Controllers/SubtitleController.cs

@@ -6,6 +6,7 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net.Mime;
 using System.Net.Mime;
+using System.Security.Cryptography;
 using System.Text;
 using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController
         [FromBody, Required] UploadSubtitleDto body)
         [FromBody, Required] UploadSubtitleDto body)
     {
     {
         var video = (Video)_libraryManager.GetItemById(itemId);
         var video = (Video)_libraryManager.GetItemById(itemId);
-        var data = Convert.FromBase64String(body.Data);
-        var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
+        await using (stream.ConfigureAwait(false))
         {
         {
             await _subtitleManager.UploadSubtitle(
             await _subtitleManager.UploadSubtitle(
                 video,
                 video,
@@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController
                     Language = body.Language,
                     Language = body.Language,
                     IsForced = body.IsForced,
                     IsForced = body.IsForced,
                     IsHearingImpaired = body.IsHearingImpaired,
                     IsHearingImpaired = body.IsHearingImpaired,
-                    Stream = memoryStream
+                    Stream = stream
                 }).ConfigureAwait(false);
                 }).ConfigureAwait(false);
             _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
             _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
 
 

+ 22 - 34
Jellyfin.Api/Controllers/SystemController.cs

@@ -4,14 +4,12 @@ using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net.Mime;
 using System.Net.Mime;
-using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.System;
@@ -27,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
 /// </summary>
 /// </summary>
 public class SystemController : BaseJellyfinApiController
 public class SystemController : BaseJellyfinApiController
 {
 {
+    private readonly ILogger<SystemController> _logger;
     private readonly IServerApplicationHost _appHost;
     private readonly IServerApplicationHost _appHost;
     private readonly IApplicationPaths _appPaths;
     private readonly IApplicationPaths _appPaths;
     private readonly IFileSystem _fileSystem;
     private readonly IFileSystem _fileSystem;
-    private readonly INetworkManager _network;
-    private readonly ILogger<SystemController> _logger;
+    private readonly INetworkManager _networkManager;
+    private readonly ISystemManager _systemManager;
 
 
     /// <summary>
     /// <summary>
     /// Initializes a new instance of the <see cref="SystemController"/> class.
     /// Initializes a new instance of the <see cref="SystemController"/> class.
     /// </summary>
     /// </summary>
-    /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+    /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+    /// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param>
     /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
     /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
     /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
     /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
-    /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
-    /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+    /// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
+    /// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
     public SystemController(
     public SystemController(
-        IServerConfigurationManager serverConfigurationManager,
+        ILogger<SystemController> logger,
         IServerApplicationHost appHost,
         IServerApplicationHost appHost,
+        IServerApplicationPaths appPaths,
         IFileSystem fileSystem,
         IFileSystem fileSystem,
-        INetworkManager network,
-        ILogger<SystemController> logger)
+        INetworkManager networkManager,
+        ISystemManager systemManager)
     {
     {
-        _appPaths = serverConfigurationManager.ApplicationPaths;
+        _logger = logger;
         _appHost = appHost;
         _appHost = appHost;
+        _appPaths = appPaths;
         _fileSystem = fileSystem;
         _fileSystem = fileSystem;
-        _network = network;
-        _logger = logger;
+        _networkManager = networkManager;
+        _systemManager = systemManager;
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -66,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     public ActionResult<SystemInfo> GetSystemInfo()
     public ActionResult<SystemInfo> GetSystemInfo()
-    {
-        return _appHost.GetSystemInfo(Request);
-    }
+        => _systemManager.GetSystemInfo(Request);
 
 
     /// <summary>
     /// <summary>
     /// Gets public information about the server.
     /// Gets public information about the server.
@@ -78,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
     [HttpGet("Info/Public")]
     [HttpGet("Info/Public")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
     public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
-    {
-        return _appHost.GetPublicSystemInfo(Request);
-    }
+        => _systemManager.GetPublicSystemInfo(Request);
 
 
     /// <summary>
     /// <summary>
     /// Pings the system.
     /// Pings the system.
@@ -91,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
     [HttpPost("Ping", Name = "PostPingSystem")]
     [HttpPost("Ping", Name = "PostPingSystem")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<string> PingSystem()
     public ActionResult<string> PingSystem()
-    {
-        return _appHost.Name;
-    }
+        => _appHost.Name;
 
 
     /// <summary>
     /// <summary>
     /// Restarts the application.
     /// Restarts the application.
@@ -107,11 +103,7 @@ public class SystemController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     public ActionResult RestartApplication()
     public ActionResult RestartApplication()
     {
     {
-        Task.Run(async () =>
-        {
-            await Task.Delay(100).ConfigureAwait(false);
-            _appHost.Restart();
-        });
+        _systemManager.Restart();
         return NoContent();
         return NoContent();
     }
     }
 
 
@@ -127,11 +119,7 @@ public class SystemController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     public ActionResult ShutdownApplication()
     public ActionResult ShutdownApplication()
     {
     {
-        Task.Run(async () =>
-        {
-            await Task.Delay(100).ConfigureAwait(false);
-            await _appHost.Shutdown().ConfigureAwait(false);
-        });
+        _systemManager.Shutdown();
         return NoContent();
         return NoContent();
     }
     }
 
 
@@ -189,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
         return new EndPointInfo
         return new EndPointInfo
         {
         {
             IsLocal = HttpContext.IsLocal(),
             IsLocal = HttpContext.IsLocal(),
-            IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
+            IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
         };
         };
     }
     }
 
 
@@ -227,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
     public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
     {
     {
-        var result = _network.GetMacAddresses()
+        var result = _networkManager.GetMacAddresses()
             .Select(i => new WakeOnLanInfo(i));
             .Select(i => new WakeOnLanInfo(i));
         return Ok(result);
         return Ok(result);
     }
     }

+ 1 - 34
Jellyfin.Api/Helpers/DynamicHlsHelper.cs

@@ -200,13 +200,6 @@ public class DynamicHlsHelper
 
 
         if (state.VideoStream is not null && state.VideoRequest is not null)
         if (state.VideoStream is not null && state.VideoRequest is not null)
         {
         {
-            // Provide a workaround for the case issue between flac and fLaC.
-            var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
-            if (!string.IsNullOrEmpty(flacWaPlaylist))
-            {
-                builder.Append(flacWaPlaylist);
-            }
-
             var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
             var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 
 
             // Provide SDR HEVC entrance for backward compatibility.
             // Provide SDR HEVC entrance for backward compatibility.
@@ -236,14 +229,7 @@ public class DynamicHlsHelper
                     }
                     }
 
 
                     var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
                     var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
-                    var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
-
-                    // Provide a workaround for the case issue between flac and fLaC.
-                    flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
-                    if (!string.IsNullOrEmpty(flacWaPlaylist))
-                    {
-                        builder.Append(flacWaPlaylist);
-                    }
+                    AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
 
 
                     // Restore the video codec
                     // Restore the video codec
                     state.OutputVideoCodec = "copy";
                     state.OutputVideoCodec = "copy";
@@ -274,13 +260,6 @@ public class DynamicHlsHelper
                 state.VideoStream.Level = originalLevel;
                 state.VideoStream.Level = originalLevel;
                 var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
                 var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
                 builder.Append(newPlaylist);
                 builder.Append(newPlaylist);
-
-                // Provide a workaround for the case issue between flac and fLaC.
-                flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
-                if (!string.IsNullOrEmpty(flacWaPlaylist))
-                {
-                    builder.Append(flacWaPlaylist);
-                }
             }
             }
         }
         }
 
 
@@ -767,16 +746,4 @@ public class DynamicHlsHelper
             newValue.ToString(),
             newValue.ToString(),
             StringComparison.Ordinal);
             StringComparison.Ordinal);
     }
     }
-
-    private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
-    {
-        if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
-        {
-            return string.Empty;
-        }
-
-        var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
-
-        return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
-    }
 }
 }

+ 5 - 3
Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs

@@ -5,7 +5,9 @@ using System.Text;
 namespace Jellyfin.Api.Helpers;
 namespace Jellyfin.Api.Helpers;
 
 
 /// <summary>
 /// <summary>
-/// Hls Codec string helpers.
+/// Helpers to generate HLS codec strings according to
+/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a>
+/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>.
 /// </summary>
 /// </summary>
 public static class HlsCodecStringHelpers
 public static class HlsCodecStringHelpers
 {
 {
@@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
     /// <summary>
     /// <summary>
     /// Codec name for FLAC.
     /// Codec name for FLAC.
     /// </summary>
     /// </summary>
-    public const string FLAC = "flac";
+    public const string FLAC = "fLaC";
 
 
     /// <summary>
     /// <summary>
     /// Codec name for ALAC.
     /// Codec name for ALAC.
@@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
     /// <summary>
     /// <summary>
     /// Codec name for OPUS.
     /// Codec name for OPUS.
     /// </summary>
     /// </summary>
-    public const string OPUS = "opus";
+    public const string OPUS = "Opus";
 
 
     /// <summary>
     /// <summary>
     /// Gets a MP3 codec string.
     /// Gets a MP3 codec string.

+ 10 - 5
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -191,6 +191,11 @@ public static class StreamingHelpers
             state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
             state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
         }
         }
 
 
+        if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
+        {
+            containerInternal = ".pcm";
+        }
+
         state.OutputAudioCodec = outputAudioCodec;
         state.OutputAudioCodec = outputAudioCodec;
         state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
         state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
         state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
         state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
@@ -243,7 +248,7 @@ public static class StreamingHelpers
             ? GetOutputFileExtension(state, mediaSource)
             ? GetOutputFileExtension(state, mediaSource)
             : ("." + state.OutputContainer);
             : ("." + state.OutputContainer);
 
 
-        state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
+        state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
 
 
         return state;
         return state;
     }
     }
@@ -418,11 +423,11 @@ public static class StreamingHelpers
     /// <returns>System.String.</returns>
     /// <returns>System.String.</returns>
     private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
     private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
     {
     {
-        var ext = Path.GetExtension(state.RequestedUrl);
+        var ext = Path.GetExtension(state.RequestedUrl.AsSpan());
 
 
-        if (!string.IsNullOrEmpty(ext))
+        if (ext.IsEmpty)
         {
         {
-            return ext;
+            return null;
         }
         }
 
 
         // Try to infer based on the desired video codec
         // Try to infer based on the desired video codec
@@ -504,7 +509,7 @@ public static class StreamingHelpers
     /// <param name="deviceId">The device id.</param>
     /// <param name="deviceId">The device id.</param>
     /// <param name="playSessionId">The play session id.</param>
     /// <param name="playSessionId">The play session id.</param>
     /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
     /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
-    private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
+    private static string GetOutputFilePath(StreamState state, string? outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
     {
     {
         var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
         var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
 
 

+ 1 - 1
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
                 await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
                 await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
             }
             }
 
 
-            if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
+            if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
             {
             {
                 string subtitlePath = state.SubtitleStream.Path;
                 string subtitlePath = state.SubtitleStream.Path;
                 string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
                 string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));

+ 1 - 2
Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs

@@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware
     /// <returns>The async task.</returns>
     /// <returns>The async task.</returns>
     public async Task Invoke(HttpContext httpContext)
     public async Task Invoke(HttpContext httpContext)
     {
     {
-        var localPath = httpContext.Request.Path.ToString();
-        if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
+        if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase))
         {
         {
             _logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
             _logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
             httpContext.Response.Redirect("web/robots.txt");
             httpContext.Response.Redirect("web/robots.txt");

+ 36 - 0
Jellyfin.Data/Enums/PersonKind.cs

@@ -94,4 +94,40 @@ public enum PersonKind
     /// A person who was the illustrator.
     /// A person who was the illustrator.
     /// </summary>
     /// </summary>
     Illustrator,
     Illustrator,
+
+    /// <summary>
+    /// A person responsible for drawing the art.
+    /// </summary>
+    Penciller,
+
+    /// <summary>
+    /// A person responsible for inking the pencil art.
+    /// </summary>
+    Inker,
+
+    /// <summary>
+    /// A person responsible for applying color to drawings.
+    /// </summary>
+    Colorist,
+
+    /// <summary>
+    /// A person responsible for drawing text and speech bubbles.
+    /// </summary>
+    Letterer,
+
+    /// <summary>
+    /// A person responsible for drawing the cover art.
+    /// </summary>
+    CoverArtist,
+
+    /// <summary>
+    /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter.
+    /// An editor may also prepare a resource for production, publication, or distribution.
+    /// </summary>
+    Editor,
+
+    /// <summary>
+    /// A person who renders a text from one language into another.
+    /// </summary>
+    Translator
 }
 }

+ 5 - 25
Jellyfin.Server.Implementations/Security/AuthorizationContext.cs

@@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security
         /// <summary>
         /// <summary>
         /// Gets the authorization.
         /// Gets the authorization.
         /// </summary>
         /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
+        /// <param name="httpContext">The HTTP context.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
+        private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
         {
         {
-            var auth = GetAuthorizationDictionary(httpReq);
-            var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
+            var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
 
 
-            httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
+            httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
             return authInfo;
             return authInfo;
         }
         }
 
 
@@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security
                 auth.TryGetValue("Token", out token);
                 auth.TryGetValue("Token", out token);
             }
             }
 
 
-#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
             if (string.IsNullOrEmpty(token))
             if (string.IsNullOrEmpty(token))
             {
             {
                 token = headers["X-Emby-Token"];
                 token = headers["X-Emby-Token"];
@@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security
                 // Request doesn't contain a token.
                 // Request doesn't contain a token.
                 return authInfo;
                 return authInfo;
             }
             }
-#pragma warning restore CA1508
 
 
             authInfo.HasToken = true;
             authInfo.HasToken = true;
             var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
             var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
@@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security
         /// <summary>
         /// <summary>
         /// Gets the auth.
         /// Gets the auth.
         /// </summary>
         /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
-        /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
-        {
-            var auth = httpReq.Request.Headers["X-Emby-Authorization"];
-
-            if (string.IsNullOrEmpty(auth))
-            {
-                auth = httpReq.Request.Headers[HeaderNames.Authorization];
-            }
-
-            return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
-        }
-
-        /// <summary>
-        /// Gets the auth.
-        /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
+        /// <param name="httpReq">The HTTP request.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
         private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
         private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
         {
         {

+ 2 - 8
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -21,7 +21,6 @@ using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Users;
 using MediaBrowser.Model.Users;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
@@ -36,7 +35,6 @@ namespace Jellyfin.Server.Implementations.Users
     {
     {
         private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
         private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
         private readonly IEventManager _eventManager;
         private readonly IEventManager _eventManager;
-        private readonly ICryptoProvider _cryptoProvider;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
         private readonly IApplicationHost _appHost;
         private readonly IApplicationHost _appHost;
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
@@ -55,7 +53,6 @@ namespace Jellyfin.Server.Implementations.Users
         /// </summary>
         /// </summary>
         /// <param name="dbProvider">The database provider.</param>
         /// <param name="dbProvider">The database provider.</param>
         /// <param name="eventManager">The event manager.</param>
         /// <param name="eventManager">The event manager.</param>
-        /// <param name="cryptoProvider">The cryptography provider.</param>
         /// <param name="networkManager">The network manager.</param>
         /// <param name="networkManager">The network manager.</param>
         /// <param name="appHost">The application host.</param>
         /// <param name="appHost">The application host.</param>
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="imageProcessor">The image processor.</param>
@@ -64,7 +61,6 @@ namespace Jellyfin.Server.Implementations.Users
         public UserManager(
         public UserManager(
             IDbContextFactory<JellyfinDbContext> dbProvider,
             IDbContextFactory<JellyfinDbContext> dbProvider,
             IEventManager eventManager,
             IEventManager eventManager,
-            ICryptoProvider cryptoProvider,
             INetworkManager networkManager,
             INetworkManager networkManager,
             IApplicationHost appHost,
             IApplicationHost appHost,
             IImageProcessor imageProcessor,
             IImageProcessor imageProcessor,
@@ -73,7 +69,6 @@ namespace Jellyfin.Server.Implementations.Users
         {
         {
             _dbProvider = dbProvider;
             _dbProvider = dbProvider;
             _eventManager = eventManager;
             _eventManager = eventManager;
-            _cryptoProvider = cryptoProvider;
             _networkManager = networkManager;
             _networkManager = networkManager;
             _appHost = appHost;
             _appHost = appHost;
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
@@ -393,7 +388,7 @@ namespace Jellyfin.Server.Implementations.Users
             }
             }
 
 
             var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
             var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
-            var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
+            var authResult = await AuthenticateLocalUser(username, password, user)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
             var authenticationProvider = authResult.AuthenticationProvider;
             var authenticationProvider = authResult.AuthenticationProvider;
             var success = authResult.Success;
             var success = authResult.Success;
@@ -803,8 +798,7 @@ namespace Jellyfin.Server.Implementations.Users
         private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser(
         private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser(
                 string username,
                 string username,
                 string password,
                 string password,
-                User? user,
-                string remoteEndPoint)
+                User? user)
         {
         {
             bool success = false;
             bool success = false;
             IAuthenticationProvider? authenticationProvider = null;
             IAuthenticationProvider? authenticationProvider = null;

+ 0 - 6
Jellyfin.Server/CoreAppHost.cs

@@ -102,9 +102,6 @@ namespace Jellyfin.Server
             base.RegisterServices(serviceCollection);
             base.RegisterServices(serviceCollection);
         }
         }
 
 
-        /// <inheritdoc />
-        protected override void RestartInternal() => Program.Restart();
-
         /// <inheritdoc />
         /// <inheritdoc />
         protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
         protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
         {
         {
@@ -114,8 +111,5 @@ namespace Jellyfin.Server
             // Jellyfin.Server.Implementations
             // Jellyfin.Server.Implementations
             yield return typeof(JellyfinDbContext).Assembly;
             yield return typeof(JellyfinDbContext).Assembly;
         }
         }
-
-        /// <inheritdoc />
-        protected override void ShutdownInternal() => Program.Shutdown();
     }
     }
 }
 }

+ 1 - 0
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -59,6 +59,7 @@ namespace Jellyfin.Server.Extensions
             serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
             serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
+            serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
 
 
             return serviceCollection.AddAuthorizationCore(options =>
             return serviceCollection.AddAuthorizationCore(options =>
             {
             {

+ 1 - 2
Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs

@@ -73,8 +73,7 @@ namespace Jellyfin.Server.Migrations.Routines
                 var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
                 var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
                 foreach (var entry in queryResult)
                 foreach (var entry in queryResult)
                 {
                 {
-                    var ratingString = entry.GetString(0);
-                    if (string.IsNullOrEmpty(ratingString))
+                    if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString))
                     {
                     {
                         connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
                         connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
                     }
                     }

+ 8 - 71
Jellyfin.Server/Program.cs

@@ -4,7 +4,6 @@ using System.Diagnostics;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Reflection;
 using System.Reflection;
-using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using CommandLine;
 using CommandLine;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations;
@@ -42,7 +41,6 @@ namespace Jellyfin.Server
         public const string LoggingConfigFileSystem = "logging.json";
         public const string LoggingConfigFileSystem = "logging.json";
 
 
         private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
         private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
-        private static CancellationTokenSource _tokenSource = new();
         private static long _startTimestamp;
         private static long _startTimestamp;
         private static ILogger _logger = NullLogger.Instance;
         private static ILogger _logger = NullLogger.Instance;
         private static bool _restartOnShutdown;
         private static bool _restartOnShutdown;
@@ -65,36 +63,9 @@ namespace Jellyfin.Server
                 .MapResult(StartApp, ErrorParsingArguments);
                 .MapResult(StartApp, ErrorParsingArguments);
         }
         }
 
 
-        /// <summary>
-        /// Shuts down the application.
-        /// </summary>
-        internal static void Shutdown()
-        {
-            if (!_tokenSource.IsCancellationRequested)
-            {
-                _tokenSource.Cancel();
-            }
-        }
-
-        /// <summary>
-        /// Restarts the application.
-        /// </summary>
-        internal static void Restart()
-        {
-            _restartOnShutdown = true;
-
-            Shutdown();
-        }
-
         private static async Task StartApp(StartupOptions options)
         private static async Task StartApp(StartupOptions options)
         {
         {
             _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);
-            AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
-
             ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
             ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
 
 
             // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
             // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
@@ -112,38 +83,10 @@ namespace Jellyfin.Server
             StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
             StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
             _logger = _loggerFactory.CreateLogger("Main");
             _logger = _loggerFactory.CreateLogger("Main");
 
 
-            // Log uncaught exceptions to the logging instead of std error
-            AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
+            // Use the logging framework for uncaught exceptions instead of std error
             AppDomain.CurrentDomain.UnhandledException += (_, e)
             AppDomain.CurrentDomain.UnhandledException += (_, e)
                 => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
                 => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
 
 
-            // Intercept Ctrl+C and Ctrl+Break
-            Console.CancelKeyPress += (_, e) =>
-            {
-                if (_tokenSource.IsCancellationRequested)
-                {
-                    return; // Already shutting down
-                }
-
-                e.Cancel = true;
-                _logger.LogInformation("Ctrl+C, shutting down");
-                Environment.ExitCode = 128 + 2;
-                Shutdown();
-            };
-
-            // Register a SIGTERM handler
-            AppDomain.CurrentDomain.ProcessExit += (_, _) =>
-            {
-                if (_tokenSource.IsCancellationRequested)
-                {
-                    return; // Already shutting down
-                }
-
-                _logger.LogInformation("Received a SIGTERM signal, shutting down");
-                Environment.ExitCode = 128 + 15;
-                Shutdown();
-            };
-
             _logger.LogInformation(
             _logger.LogInformation(
                 "Jellyfin version: {Version}",
                 "Jellyfin version: {Version}",
                 Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
                 Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
@@ -173,12 +116,10 @@ namespace Jellyfin.Server
 
 
             do
             do
             {
             {
-                _restartOnShutdown = false;
                 await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
                 await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
 
 
                 if (_restartOnShutdown)
                 if (_restartOnShutdown)
                 {
                 {
-                    _tokenSource = new CancellationTokenSource();
                     _startTimestamp = Stopwatch.GetTimestamp();
                     _startTimestamp = Stopwatch.GetTimestamp();
                 }
                 }
             } while (_restartOnShutdown);
             } while (_restartOnShutdown);
@@ -186,7 +127,7 @@ namespace Jellyfin.Server
 
 
         private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
         private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
         {
         {
-            var appHost = new CoreAppHost(
+            using var appHost = new CoreAppHost(
                 appPaths,
                 appPaths,
                 _loggerFactory,
                 _loggerFactory,
                 options,
                 options,
@@ -196,6 +137,7 @@ namespace Jellyfin.Server
             try
             try
             {
             {
                 host = Host.CreateDefaultBuilder()
                 host = Host.CreateDefaultBuilder()
+                    .UseConsoleLifetime()
                     .ConfigureServices(services => appHost.Init(services))
                     .ConfigureServices(services => appHost.Init(services))
                     .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
                     .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
                     .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
                     .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
@@ -210,7 +152,7 @@ namespace Jellyfin.Server
 
 
                 try
                 try
                 {
                 {
-                    await host.StartAsync(_tokenSource.Token).ConfigureAwait(false);
+                    await host.StartAsync().ConfigureAwait(false);
 
 
                     if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
                     if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
                     {
                     {
@@ -219,22 +161,18 @@ namespace Jellyfin.Server
                         StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger);
                         StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger);
                     }
                     }
                 }
                 }
-                catch (Exception ex) when (ex is not TaskCanceledException)
+                catch (Exception)
                 {
                 {
                     _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;
                     throw;
                 }
                 }
 
 
-                await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
+                await appHost.RunStartupTasksAsync().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);
-            }
-            catch (TaskCanceledException)
-            {
-                // Don't throw on cancellation
+                await host.WaitForShutdownAsync().ConfigureAwait(false);
+                _restartOnShutdown = appHost.ShouldRestart;
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -257,7 +195,6 @@ namespace Jellyfin.Server
                     }
                     }
                 }
                 }
 
 
-                await appHost.DisposeAsync().ConfigureAwait(false);
                 host?.Dispose();
                 host?.Dispose();
             }
             }
         }
         }

+ 4 - 56
MediaBrowser.Common/Extensions/ProcessExtensions.cs

@@ -15,65 +15,13 @@ namespace MediaBrowser.Common.Extensions
         /// </summary>
         /// </summary>
         /// <param name="process">The process to wait for.</param>
         /// <param name="process">The process to wait for.</param>
         /// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
         /// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
-        /// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns>
-        /// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception>
-        public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout)
+        /// <returns>A task that will complete when the process has exited, cancellation has been requested, or an error occurs.</returns>
+        /// <exception cref="OperationCanceledException">The timeout ended.</exception>
+        public static async Task WaitForExitAsync(this Process process, TimeSpan timeout)
         {
         {
             using (var cancelTokenSource = new CancellationTokenSource(timeout))
             using (var cancelTokenSource = new CancellationTokenSource(timeout))
             {
             {
-                return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false);
-            }
-        }
-
-        /// <summary>
-        /// Asynchronously wait for the process to exit.
-        /// </summary>
-        /// <param name="process">The process to wait for.</param>
-        /// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param>
-        /// <returns>True if the task exited normally, false if cancelled before the process exited.</returns>
-        public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken)
-        {
-            if (!process.EnableRaisingEvents)
-            {
-                throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit.");
-            }
-
-            // Add an event handler for the process exit event
-            var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
-            process.Exited += (_, _) => tcs.TrySetResult(true);
-
-            // Return immediately if the process has already exited
-            if (process.HasExitedSafe())
-            {
-                return true;
-            }
-
-            // Register with the cancellation token then await
-            using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe())))
-            {
-                return await tcs.Task.ConfigureAwait(false);
-            }
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether the associated process has been terminated using
-        /// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process
-        /// associated with the <see cref="Process"/>.
-        /// </summary>
-        /// <param name="process">The process to check the exit status for.</param>
-        /// <returns>
-        /// True if the operating system process referenced by the <see cref="Process"/> component has
-        /// terminated, or if there is no associated operating system process; otherwise, false.
-        /// </returns>
-        private static bool HasExitedSafe(this Process process)
-        {
-            try
-            {
-                return process.HasExited;
-            }
-            catch (InvalidOperationException)
-            {
-                return true;
+                await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false);
             }
             }
         }
         }
     }
     }

+ 4 - 17
MediaBrowser.Common/IApplicationHost.cs

@@ -1,7 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Reflection;
 using System.Reflection;
-using System.Threading.Tasks;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 
 
 namespace MediaBrowser.Common
 namespace MediaBrowser.Common
@@ -36,16 +35,15 @@ namespace MediaBrowser.Common
         string SystemId { get; }
         string SystemId { get; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets a value indicating whether this instance has pending kernel reload.
+        /// Gets a value indicating whether this instance has pending changes requiring a restart.
         /// </summary>
         /// </summary>
-        /// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value>
+        /// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value>
         bool HasPendingRestart { get; }
         bool HasPendingRestart { get; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets a value indicating whether this instance is currently shutting down.
+        /// Gets or sets a value indicating whether the application should restart.
         /// </summary>
         /// </summary>
-        /// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value>
-        bool IsShuttingDown { get; }
+        bool ShouldRestart { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the application version.
         /// Gets the application version.
@@ -87,11 +85,6 @@ namespace MediaBrowser.Common
         /// </summary>
         /// </summary>
         void NotifyPendingRestart();
         void NotifyPendingRestart();
 
 
-        /// <summary>
-        /// Restarts this instance.
-        /// </summary>
-        void Restart();
-
         /// <summary>
         /// <summary>
         /// Gets the exports.
         /// Gets the exports.
         /// </summary>
         /// </summary>
@@ -123,12 +116,6 @@ namespace MediaBrowser.Common
         /// <returns>``0.</returns>
         /// <returns>``0.</returns>
         T Resolve<T>();
         T Resolve<T>();
 
 
-        /// <summary>
-        /// Shuts down.
-        /// </summary>
-        /// <returns>A task.</returns>
-        Task Shutdown();
-
         /// <summary>
         /// <summary>
         /// Initializes this instance.
         /// Initializes this instance.
         /// </summary>
         /// </summary>

+ 0 - 5
MediaBrowser.Common/Plugins/IPluginManager.cs

@@ -29,11 +29,6 @@ namespace MediaBrowser.Common.Plugins
         /// <returns>An IEnumerable{Assembly}.</returns>
         /// <returns>An IEnumerable{Assembly}.</returns>
         IEnumerable<Assembly> LoadAssemblies();
         IEnumerable<Assembly> LoadAssemblies();
 
 
-        /// <summary>
-        /// Unloads all of the assemblies.
-        /// </summary>
-        void UnloadAssemblies();
-
         /// <summary>
         /// <summary>
         /// Registers the plugin's services with the DI.
         /// Registers the plugin's services with the DI.
         /// Note: DI is not yet instantiated yet.
         /// Note: DI is not yet instantiated yet.

+ 0 - 11
MediaBrowser.Controller/Drawing/IImageProcessor.cs

@@ -2,7 +2,6 @@
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.IO;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing
 
 
         string? GetImageCacheTag(User user);
         string? GetImageCacheTag(User user);
 
 
-        /// <summary>
-        /// Processes the image.
-        /// </summary>
-        /// <param name="options">The options.</param>
-        /// <param name="toStream">To stream.</param>
-        /// <returns>Task.</returns>
-        Task ProcessImage(ImageProcessingOptions options, Stream toStream);
-
         /// <summary>
         /// <summary>
         /// Processes the image.
         /// Processes the image.
         /// </summary>
         /// </summary>
@@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="options">The options.</param>
         /// <param name="options">The options.</param>
         /// <param name="libraryName">The library name to draw onto the collage.</param>
         /// <param name="libraryName">The library name to draw onto the collage.</param>
         void CreateImageCollage(ImageCollageOptions options, string? libraryName);
         void CreateImageCollage(ImageCollageOptions options, string? libraryName);
-
-        bool SupportsTransparency(string path);
     }
     }
 }
 }

+ 2 - 1
MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs

@@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing
         private bool IsFormatSupported(string originalImagePath)
         private bool IsFormatSupported(string originalImagePath)
         {
         {
             var ext = Path.GetExtension(originalImagePath);
             var ext = Path.GetExtension(originalImagePath);
-            return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase));
+            ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase);
+            return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase));
         }
         }
     }
     }
 }
 }

+ 12 - 30
MediaBrowser.Controller/Entities/CollectionFolder.cs

@@ -3,6 +3,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
@@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities
     public class CollectionFolder : Folder, ICollectionFolder
     public class CollectionFolder : Folder, ICollectionFolder
     {
     {
         private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
         private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
-        private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>();
+        private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>();
         private bool _requiresRefresh;
         private bool _requiresRefresh;
 
 
         /// <summary>
         /// <summary>
@@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities
         }
         }
 
 
         public static LibraryOptions GetLibraryOptions(string path)
         public static LibraryOptions GetLibraryOptions(string path)
-        {
-            lock (_libraryOptions)
-            {
-                if (!_libraryOptions.TryGetValue(path, out var options))
-                {
-                    options = LoadLibraryOptions(path);
-                    _libraryOptions[path] = options;
-                }
-
-                return options;
-            }
-        }
+            => _libraryOptions.GetOrAdd(path, LoadLibraryOptions);
 
 
         public static void SaveLibraryOptions(string path, LibraryOptions options)
         public static void SaveLibraryOptions(string path, LibraryOptions options)
         {
         {
-            lock (_libraryOptions)
-            {
-                _libraryOptions[path] = options;
+            _libraryOptions[path] = options;
 
 
-                var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
-                foreach (var mediaPath in clone.PathInfos)
+            var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
+            foreach (var mediaPath in clone.PathInfos)
+            {
+                if (!string.IsNullOrEmpty(mediaPath.Path))
                 {
                 {
-                    if (!string.IsNullOrEmpty(mediaPath.Path))
-                    {
-                        mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
-                    }
+                    mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
                 }
                 }
-
-                XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
             }
             }
+
+            XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
         }
         }
 
 
         public static void OnCollectionFolderChange()
         public static void OnCollectionFolderChange()
-        {
-            lock (_libraryOptions)
-            {
-                _libraryOptions.Clear();
-            }
-        }
+            => _libraryOptions.Clear();
 
 
         public override bool IsSaveLocalMetadataEnabled()
         public override bool IsSaveLocalMetadataEnabled()
         {
         {

+ 193 - 0
MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs

@@ -0,0 +1,193 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Xml;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Extensions;
+
+/// <summary>
+/// Provides extension methods for <see cref="XmlReader"/> to parse <see cref="BaseItem"/>'s.
+/// </summary>
+public static class XmlReaderExtensions
+{
+    /// <summary>
+    /// Reads a trimmed string from the current node.
+    /// </summary>
+    /// <param name="reader">The <see cref="XmlReader"/>.</param>
+    /// <returns>The trimmed content.</returns>
+    public static string ReadNormalizedString(this XmlReader reader)
+    {
+        ArgumentNullException.ThrowIfNull(reader);
+
+        return reader.ReadElementContentAsString().Trim();
+    }
+
+    /// <summary>
+    /// Reads an int from the current node.
+    /// </summary>
+    /// <param name="reader">The <see cref="XmlReader"/>.</param>
+    /// <param name="value">The parsed <c>int</c>.</param>
+    /// <returns>A value indicating whether the parsing succeeded.</returns>
+    public static bool TryReadInt(this XmlReader reader, out int value)
+    {
+        ArgumentNullException.ThrowIfNull(reader);
+
+        return int.TryParse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture, out value);
+    }
+
+    /// <summary>
+    /// Parses a <see cref="DateTime"/> from the current node.
+    /// </summary>
+    /// <param name="reader">The <see cref="XmlReader"/>.</param>
+    /// <param name="value">The parsed <see cref="DateTime"/>.</param>
+    /// <returns>A value indicating whether the parsing succeeded.</returns>
+    public static bool TryReadDateTime(this XmlReader reader, out DateTime value)
+    {
+        ArgumentNullException.ThrowIfNull(reader);
+
+        return DateTime.TryParse(
+            reader.ReadElementContentAsString(),
+            CultureInfo.InvariantCulture,
+            DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
+            out value);
+    }
+
+    /// <summary>
+    /// Parses a <see cref="DateTime"/> from the current node.
+    /// </summary>
+    /// <param name="reader">The <see cref="XmlReader"/>.</param>
+    /// <param name="formatString">The date format string.</param>
+    /// <param name="value">The parsed <see cref="DateTime"/>.</param>
+    /// <returns>A value indicating whether the parsing succeeded.</returns>
+    public static bool TryReadDateTimeExact(this XmlReader reader, string formatString, out DateTime value)
+    {
+        ArgumentNullException.ThrowIfNull(reader);
+        ArgumentNullException.ThrowIfNull(formatString);
+
+        return DateTime.TryParseExact(
+            reader.ReadElementContentAsString(),
+            formatString,
+            CultureInfo.InvariantCulture,
+            DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
+            out value);
+    }
+
+    /// <summary>
+    /// Parses a <see cref="PersonInfo"/> from the xml node.
+    /// </summary>
+    /// <param name="reader">The <see cref="XmlReader"/>.</param>
+    /// <returns>A <see cref="PersonInfo"/>, or <c>null</c> if none is found.</returns>
+    public static PersonInfo? GetPersonFromXmlNode(this XmlReader reader)
+    {
+        ArgumentNullException.ThrowIfNull(reader);
+
+        if (reader.IsEmptyElement)
+        {
+            reader.Read();
+            return null;
+        }
+
+        var name = string.Empty;
+        var type = PersonKind.Actor;  // If type is not specified assume actor
+        var role = string.Empty;
+        int? sortOrder = null;
+        string? imageUrl = null;
+
+        using var subtree = reader.ReadSubtree();
+        subtree.MoveToContent();
+        subtree.Read();
+
+        while (subtree is { EOF: false, ReadState: ReadState.Interactive })
+        {
+            if (subtree.NodeType != XmlNodeType.Element)
+            {
+                subtree.Read();
+                continue;
+            }
+
+            switch (subtree.Name)
+            {
+                case "name":
+                case "Name":
+                    name = subtree.ReadNormalizedString();
+                    break;
+                case "role":
+                case "Role":
+                    role = subtree.ReadNormalizedString();
+                    break;
+                case "type":
+                case "Type":
+                    Enum.TryParse(subtree.ReadElementContentAsString(), true, out type);
+                    break;
+                case "order":
+                case "sortorder":
+                case "SortOrder":
+                    if (subtree.TryReadInt(out var sortOrderVal))
+                    {
+                        sortOrder = sortOrderVal;
+                    }
+
+                    break;
+                case "thumb":
+                    imageUrl = subtree.ReadNormalizedString();
+                    break;
+                default:
+                    subtree.Skip();
+                    break;
+            }
+        }
+
+        if (string.IsNullOrWhiteSpace(name))
+        {
+            return null;
+        }
+
+        return new PersonInfo
+        {
+            Name = name,
+            Role = role,
+            Type = type,
+            SortOrder = sortOrder,
+            ImageUrl = imageUrl
+        };
+    }
+
+    /// <summary>
+    /// Used to split names of comma or pipe delimited genres and people.
+    /// </summary>
+    /// <param name="reader">The <see cref="XmlReader"/>.</param>
+    /// <returns>IEnumerable{System.String}.</returns>
+    public static IEnumerable<string> GetStringArray(this XmlReader reader)
+    {
+        ArgumentNullException.ThrowIfNull(reader);
+        var value = reader.ReadElementContentAsString();
+
+        // Only split by comma if there is no pipe in the string
+        // We have to be careful to not split names like Matthew, Jr.
+        var separator = !value.Contains('|', StringComparison.Ordinal)
+            && !value.Contains(';', StringComparison.Ordinal)
+                ? new[] { ',' }
+                : new[] { '|', ';' };
+
+        foreach (var part in value.Trim().Trim(separator).Split(separator))
+        {
+            if (!string.IsNullOrWhiteSpace(part))
+            {
+                yield return part.Trim();
+            }
+        }
+    }
+
+    /// <summary>
+    /// Parses a <see cref="PersonInfo"/> array from the xml node.
+    /// </summary>
+    /// <param name="reader">The <see cref="XmlReader"/>.</param>
+    /// <param name="personKind">The <see cref="PersonKind"/>.</param>
+    /// <returns>The <see cref="IEnumerable{PersonInfo}"/>.</returns>
+    public static IEnumerable<PersonInfo> GetPersonArray(this XmlReader reader, PersonKind personKind)
+        => reader.GetStringArray()
+            .Select(part => new PersonInfo { Name = part, Type = personKind });
+}

+ 0 - 12
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -4,7 +4,6 @@
 
 
 using System.Net;
 using System.Net;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
-using MediaBrowser.Model.System;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 
 
 namespace MediaBrowser.Controller
 namespace MediaBrowser.Controller
@@ -16,8 +15,6 @@ namespace MediaBrowser.Controller
     {
     {
         bool CoreStartupHasCompleted { get; }
         bool CoreStartupHasCompleted { get; }
 
 
-        bool CanLaunchWebBrowser { get; }
-
         /// <summary>
         /// <summary>
         /// Gets the HTTP server port.
         /// Gets the HTTP server port.
         /// </summary>
         /// </summary>
@@ -41,15 +38,6 @@ namespace MediaBrowser.Controller
         /// <value>The name of the friendly.</value>
         /// <value>The name of the friendly.</value>
         string FriendlyName { get; }
         string FriendlyName { get; }
 
 
-        /// <summary>
-        /// Gets the system info.
-        /// </summary>
-        /// <param name="request">The HTTP request.</param>
-        /// <returns>SystemInfo.</returns>
-        SystemInfo GetSystemInfo(HttpRequest request);
-
-        PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
-
         /// <summary>
         /// <summary>
         /// Gets a URL specific for the request.
         /// Gets a URL specific for the request.
         /// </summary>
         /// </summary>

+ 34 - 0
MediaBrowser.Controller/ISystemManager.cs

@@ -0,0 +1,34 @@
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Http;
+
+namespace MediaBrowser.Controller;
+
+/// <summary>
+/// A service for managing the application instance.
+/// </summary>
+public interface ISystemManager
+{
+    /// <summary>
+    /// Gets the system info.
+    /// </summary>
+    /// <param name="request">The HTTP request.</param>
+    /// <returns>The <see cref="SystemInfo"/>.</returns>
+    SystemInfo GetSystemInfo(HttpRequest request);
+
+    /// <summary>
+    /// Gets the public system info.
+    /// </summary>
+    /// <param name="request">The HTTP request.</param>
+    /// <returns>The <see cref="PublicSystemInfo"/>.</returns>
+    PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
+
+    /// <summary>
+    /// Starts the application restart process.
+    /// </summary>
+    void Restart();
+
+    /// <summary>
+    /// Starts the application shutdown process.
+    /// </summary>
+    void Shutdown();
+}

+ 32 - 10
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -48,6 +48,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
         private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
         private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
         private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
         private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
         private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
+        private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
 
 
         private static readonly string[] _videoProfilesH264 = new[]
         private static readonly string[] _videoProfilesH264 = new[]
         {
         {
@@ -547,25 +548,25 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <returns>System.Nullable{VideoCodecs}.</returns>
         /// <returns>System.Nullable{VideoCodecs}.</returns>
         public string InferVideoCodec(string url)
         public string InferVideoCodec(string url)
         {
         {
-            var ext = Path.GetExtension(url);
+            var ext = Path.GetExtension(url.AsSpan());
 
 
-            if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase))
+            if (ext.Equals(".asf", StringComparison.OrdinalIgnoreCase))
             {
             {
                 return "wmv";
                 return "wmv";
             }
             }
 
 
-            if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
+            if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase))
             {
             {
                 // TODO: this may not always mean VP8, as the codec ages
                 // TODO: this may not always mean VP8, as the codec ages
                 return "vp8";
                 return "vp8";
             }
             }
 
 
-            if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
+            if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase))
             {
             {
                 return "theora";
                 return "theora";
             }
             }
 
 
-            if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase))
+            if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ts", StringComparison.OrdinalIgnoreCase))
             {
             {
                 return "h264";
                 return "h264";
             }
             }
@@ -1079,10 +1080,10 @@ namespace MediaBrowser.Controller.MediaEncoding
                 && state.SubtitleStream.IsExternal)
                 && state.SubtitleStream.IsExternal)
             {
             {
                 var subtitlePath = state.SubtitleStream.Path;
                 var subtitlePath = state.SubtitleStream.Path;
-                var subtitleExtension = Path.GetExtension(subtitlePath);
+                var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
 
 
-                if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase))
+                if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
+                    || subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
                     var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
                     if (File.Exists(idxFile))
                     if (File.Exists(idxFile))
@@ -2006,6 +2007,14 @@ namespace MediaBrowser.Controller.MediaEncoding
                 param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0";
                 param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0";
             }
             }
 
 
+            /* Access unit too large: 8192 < 20880 error */
+            if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) ||
+                 string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) &&
+                 _mediaEncoder.EncoderVersion >= _minFFmpegVaapiH26xEncA53CcSei)
+            {
+                param += " -sei -a53_cc";
+            }
+
             return param;
             return param;
         }
         }
 
 
@@ -5681,7 +5690,6 @@ namespace MediaBrowser.Controller.MediaEncoding
 
 
             // Apply -analyzeduration as per the environment variable,
             // Apply -analyzeduration as per the environment variable,
             // otherwise ffmpeg will break on certain files due to default value is 0.
             // otherwise ffmpeg will break on certain files due to default value is 0.
-            // The default value of -probesize is more than enough, so leave it as is.
             var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
             var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
 
 
             if (state.MediaSource.AnalyzeDurationMs > 0)
             if (state.MediaSource.AnalyzeDurationMs > 0)
@@ -5700,6 +5708,14 @@ namespace MediaBrowser.Controller.MediaEncoding
 
 
             inputModifier = inputModifier.Trim();
             inputModifier = inputModifier.Trim();
 
 
+            // Apply -probesize if configured
+            var ffmpegProbeSize = _config.GetFFmpegProbeSize();
+
+            if (!string.IsNullOrEmpty(ffmpegProbeSize))
+            {
+                inputModifier += $" -probesize {ffmpegProbeSize}";
+            }
+
             var userAgentParam = GetUserAgentParam(state);
             var userAgentParam = GetUserAgentParam(state);
 
 
             if (!string.IsNullOrEmpty(userAgentParam))
             if (!string.IsNullOrEmpty(userAgentParam))
@@ -6024,7 +6040,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             var format = string.Empty;
             var format = string.Empty;
             var keyFrame = string.Empty;
             var keyFrame = string.Empty;
 
 
-            if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase)
+            if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase)
                 && state.BaseRequest.Context == EncodingContext.Streaming)
                 && state.BaseRequest.Context == EncodingContext.Streaming)
             {
             {
                 // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
                 // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
@@ -6233,6 +6249,12 @@ namespace MediaBrowser.Controller.MediaEncoding
                 audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
                 audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
             }
             }
 
 
+            if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal))
+            {
+                audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4)));
+                audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
+            }
+
             if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
             if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
             {
             {
                 // opus only supports specific sampling rates
                 // opus only supports specific sampling rates

+ 1 - 1
MediaBrowser.Controller/Resolvers/IItemResolver.cs

@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers
         /// </summary>
         /// </summary>
         /// <param name="args">The args.</param>
         /// <param name="args">The args.</param>
         /// <returns>BaseItem.</returns>
         /// <returns>BaseItem.</returns>
-        BaseItem ResolvePath(ItemResolveArgs args);
+        BaseItem? ResolvePath(ItemResolveArgs args);
     }
     }
 
 
     public interface IMultiItemResolver
     public interface IMultiItemResolver

+ 2 - 4
MediaBrowser.Controller/Resolvers/ItemResolver.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 
 
@@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers
         /// </summary>
         /// </summary>
         /// <param name="args">The args.</param>
         /// <param name="args">The args.</param>
         /// <returns>`0.</returns>
         /// <returns>`0.</returns>
-        protected internal virtual T Resolve(ItemResolveArgs args)
+        protected internal virtual T? Resolve(ItemResolveArgs args)
         {
         {
             return null;
             return null;
         }
         }
@@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers
         /// </summary>
         /// </summary>
         /// <param name="args">The args.</param>
         /// <param name="args">The args.</param>
         /// <returns>BaseItem.</returns>
         /// <returns>BaseItem.</returns>
-        public BaseItem ResolvePath(ItemResolveArgs args)
+        public BaseItem? ResolvePath(ItemResolveArgs args)
         {
         {
             var item = Resolve(args);
             var item = Resolve(args);
 
 

+ 0 - 14
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -232,20 +232,6 @@ namespace MediaBrowser.Controller.Session
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         Task SendRestartRequiredNotification(CancellationToken cancellationToken);
         Task SendRestartRequiredNotification(CancellationToken cancellationToken);
 
 
-        /// <summary>
-        /// Sends the server shutdown notification.
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendServerShutdownNotification(CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Sends the server restart notification.
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendServerRestartNotification(CancellationToken cancellationToken);
-
         /// <summary>
         /// <summary>
         /// Adds the additional user.
         /// Adds the additional user.
         /// </summary>
         /// </summary>

+ 62 - 443
MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs

@@ -9,6 +9,7 @@ using System.Xml;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
@@ -128,42 +129,19 @@ namespace MediaBrowser.LocalMetadata.Parsers
 
 
             switch (reader.Name)
             switch (reader.Name)
             {
             {
-                // DateCreated
                 case "Added":
                 case "Added":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(val))
+                    if (reader.TryReadDateTime(out var dateCreated))
                     {
                     {
-                        if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added))
-                        {
-                            item.DateCreated = added;
-                        }
-                        else
-                        {
-                            Logger.LogWarning("Invalid Added value found: {Value}", val);
-                        }
+                        item.DateCreated = dateCreated;
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "OriginalTitle":
                 case "OriginalTitle":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrEmpty(val))
-                    {
-                        item.OriginalTitle = val;
-                    }
-
+                    item.OriginalTitle = reader.ReadNormalizedString();
                     break;
                     break;
-                }
-
                 case "LocalTitle":
                 case "LocalTitle":
-                    item.Name = reader.ReadElementContentAsString();
+                    item.Name = reader.ReadNormalizedString();
                     break;
                     break;
-
                 case "CriticRating":
                 case "CriticRating":
                 {
                 {
                     var text = reader.ReadElementContentAsString();
                     var text = reader.ReadElementContentAsString();
@@ -177,63 +155,26 @@ namespace MediaBrowser.LocalMetadata.Parsers
                 }
                 }
 
 
                 case "SortTitle":
                 case "SortTitle":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(val))
-                    {
-                        item.ForcedSortName = val;
-                    }
-
+                    item.ForcedSortName = reader.ReadNormalizedString();
                     break;
                     break;
-                }
-
                 case "Overview":
                 case "Overview":
                 case "Description":
                 case "Description":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(val))
-                    {
-                        item.Overview = val;
-                    }
-
+                    item.Overview = reader.ReadNormalizedString();
                     break;
                     break;
-                }
-
                 case "Language":
                 case "Language":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    item.PreferredMetadataLanguage = val;
-
+                    item.PreferredMetadataLanguage = reader.ReadNormalizedString();
                     break;
                     break;
-                }
-
                 case "CountryCode":
                 case "CountryCode":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    item.PreferredMetadataCountryCode = val;
-
+                    item.PreferredMetadataCountryCode = reader.ReadNormalizedString();
                     break;
                     break;
-                }
-
                 case "PlaceOfBirth":
                 case "PlaceOfBirth":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(val))
+                    var placeOfBirth = reader.ReadNormalizedString();
+                    if (!string.IsNullOrEmpty(placeOfBirth) && item is Person person)
                     {
                     {
-                        if (item is Person person)
-                        {
-                            person.ProductionLocations = new[] { val };
-                        }
+                        person.ProductionLocations = new[] { placeOfBirth };
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "LockedFields":
                 case "LockedFields":
                 {
                 {
                     var val = reader.ReadElementContentAsString();
                     var val = reader.ReadElementContentAsString();
@@ -275,10 +216,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
                 {
                 {
                     if (!reader.IsEmptyElement)
                     if (!reader.IsEmptyElement)
                     {
                     {
-                        using (var subtree = reader.ReadSubtree())
-                        {
-                            FetchFromCountriesNode(subtree);
-                        }
+                        reader.Skip();
                     }
                     }
                     else
                     else
                     {
                     {
@@ -290,183 +228,84 @@ namespace MediaBrowser.LocalMetadata.Parsers
 
 
                 case "ContentRating":
                 case "ContentRating":
                 case "MPAARating":
                 case "MPAARating":
-                {
-                    var rating = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(rating))
-                    {
-                        item.OfficialRating = rating;
-                    }
-
+                    item.OfficialRating = reader.ReadNormalizedString();
                     break;
                     break;
-                }
-
                 case "CustomRating":
                 case "CustomRating":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(val))
-                    {
-                        item.CustomRating = val;
-                    }
-
+                    item.CustomRating = reader.ReadNormalizedString();
                     break;
                     break;
-                }
-
                 case "RunningTime":
                 case "RunningTime":
-                {
-                    var text = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(text))
+                    var runtimeText = reader.ReadElementContentAsString();
+                    if (!string.IsNullOrWhiteSpace(runtimeText))
                     {
                     {
-                        if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
+                        if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
                         {
                         {
                             item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
                             item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
                         }
                         }
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "AspectRatio":
                 case "AspectRatio":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(val) && item is IHasAspectRatio hasAspectRatio)
+                    var aspectRatio = reader.ReadNormalizedString();
+                    if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio)
                     {
                     {
-                        hasAspectRatio.AspectRatio = val;
+                        hasAspectRatio.AspectRatio = aspectRatio;
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "LockData":
                 case "LockData":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(val))
-                    {
-                        item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
-                    }
-
+                    item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
                     break;
                     break;
-                }
-
                 case "Network":
                 case "Network":
-                {
-                    foreach (var name in SplitNames(reader.ReadElementContentAsString()))
+                    foreach (var name in reader.GetStringArray())
                     {
                     {
-                        if (string.IsNullOrWhiteSpace(name))
-                        {
-                            continue;
-                        }
-
                         item.AddStudio(name);
                         item.AddStudio(name);
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "Director":
                 case "Director":
-                {
-                    foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
+                    foreach (var director in reader.GetPersonArray(PersonKind.Director))
                     {
                     {
-                        if (string.IsNullOrWhiteSpace(p.Name))
-                        {
-                            continue;
-                        }
-
-                        itemResult.AddPerson(p);
+                        itemResult.AddPerson(director);
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "Writer":
                 case "Writer":
-                {
-                    foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
+                    foreach (var writer in reader.GetPersonArray(PersonKind.Writer))
                     {
                     {
-                        if (string.IsNullOrWhiteSpace(p.Name))
-                        {
-                            continue;
-                        }
-
-                        itemResult.AddPerson(p);
+                        itemResult.AddPerson(writer);
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "Actors":
                 case "Actors":
-                {
-                    var actors = reader.ReadInnerXml();
-
-                    if (actors.Contains('<', StringComparison.Ordinal))
+                    foreach (var actor in reader.GetPersonArray(PersonKind.Actor))
                     {
                     {
-                        // This is one of the mis-named "Actors" full nodes created by MB2
-                        // Create a reader and pass it to the persons node processor
-                        using var xmlReader = XmlReader.Create(new StringReader($"<Persons>{actors}</Persons>"));
-                        FetchDataFromPersonsNode(xmlReader, itemResult);
-                    }
-                    else
-                    {
-                        // Old-style piped string
-                        foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor }))
-                        {
-                            if (string.IsNullOrWhiteSpace(p.Name))
-                            {
-                                continue;
-                            }
-
-                            itemResult.AddPerson(p);
-                        }
+                        itemResult.AddPerson(actor);
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "GuestStars":
                 case "GuestStars":
-                {
-                    foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar }))
+                    foreach (var guestStar in reader.GetPersonArray(PersonKind.GuestStar))
                     {
                     {
-                        if (string.IsNullOrWhiteSpace(p.Name))
-                        {
-                            continue;
-                        }
-
-                        itemResult.AddPerson(p);
+                        itemResult.AddPerson(guestStar);
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "Trailer":
                 case "Trailer":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(val))
+                    var trailer = reader.ReadNormalizedString();
+                    if (!string.IsNullOrEmpty(trailer))
                     {
                     {
-                        item.AddTrailerUrl(val);
+                        item.AddTrailerUrl(trailer);
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "DisplayOrder":
                 case "DisplayOrder":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (item is IHasDisplayOrder hasDisplayOrder)
+                    var displayOrder = reader.ReadNormalizedString();
+                    if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder)
                     {
                     {
-                        if (!string.IsNullOrWhiteSpace(val))
-                        {
-                            hasDisplayOrder.DisplayOrder = val;
-                        }
+                        hasDisplayOrder.DisplayOrder = displayOrder;
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "Trailers":
                 case "Trailers":
                 {
                 {
                     if (!reader.IsEmptyElement)
                     if (!reader.IsEmptyElement)
@@ -483,20 +322,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
                 }
                 }
 
 
                 case "ProductionYear":
                 case "ProductionYear":
-                {
-                    var val = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(val))
+                    if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
                     {
                     {
-                        if (int.TryParse(val, out var productionYear) && productionYear > 1850)
-                        {
-                            item.ProductionYear = productionYear;
-                        }
+                        item.ProductionYear = productionYear;
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "Rating":
                 case "Rating":
                 case "IMDBrating":
                 case "IMDBrating":
                 {
                 {
@@ -517,40 +348,24 @@ namespace MediaBrowser.LocalMetadata.Parsers
                 case "BirthDate":
                 case "BirthDate":
                 case "PremiereDate":
                 case "PremiereDate":
                 case "FirstAired":
                 case "FirstAired":
-                {
-                    var firstAired = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(firstAired))
+                    if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var firstAired))
                     {
                     {
-                        if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
-                        {
-                            item.PremiereDate = airDate;
-                            item.ProductionYear = airDate.Year;
-                        }
+                        item.PremiereDate = firstAired;
+                        item.ProductionYear = firstAired.Year;
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "DeathDate":
                 case "DeathDate":
                 case "EndDate":
                 case "EndDate":
-                {
-                    var firstAired = reader.ReadElementContentAsString();
-
-                    if (!string.IsNullOrWhiteSpace(firstAired))
+                    if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var endDate))
                     {
                     {
-                        if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
-                        {
-                            item.EndDate = airDate;
-                        }
+                        item.EndDate = endDate;
                     }
                     }
 
 
                     break;
                     break;
-                }
-
                 case "CollectionNumber":
                 case "CollectionNumber":
-                    var tmdbCollection = reader.ReadElementContentAsString();
-                    if (!string.IsNullOrWhiteSpace(tmdbCollection))
+                    var tmdbCollection = reader.ReadNormalizedString();
+                    if (!string.IsNullOrEmpty(tmdbCollection))
                     {
                     {
                         item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
                         item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
                     }
                     }
@@ -753,41 +568,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
             item.Shares = list.ToArray();
             item.Shares = list.ToArray();
         }
         }
 
 
-        private void FetchFromCountriesNode(XmlReader reader)
-        {
-            reader.MoveToContent();
-            reader.Read();
-
-            // Loop through each element
-            while (!reader.EOF && reader.ReadState == ReadState.Interactive)
-            {
-                if (reader.NodeType == XmlNodeType.Element)
-                {
-                    switch (reader.Name)
-                    {
-                        case "Country":
-                        {
-                            var val = reader.ReadElementContentAsString();
-
-                            if (!string.IsNullOrWhiteSpace(val))
-                            {
-                            }
-
-                            break;
-                        }
-
-                        default:
-                            reader.Skip();
-                            break;
-                    }
-                }
-                else
-                {
-                    reader.Read();
-                }
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Fetches from taglines node.
         /// Fetches from taglines node.
         /// </summary>
         /// </summary>
@@ -806,17 +586,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     switch (reader.Name)
                     switch (reader.Name)
                     {
                     {
                         case "Tagline":
                         case "Tagline":
-                        {
-                            var val = reader.ReadElementContentAsString();
-
-                            if (!string.IsNullOrWhiteSpace(val))
-                            {
-                                item.Tagline = val;
-                            }
-
+                            item.Tagline = reader.ReadNormalizedString();
                             break;
                             break;
-                        }
-
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
                             break;
                             break;
@@ -847,17 +618,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     switch (reader.Name)
                     switch (reader.Name)
                     {
                     {
                         case "Genre":
                         case "Genre":
-                        {
-                            var genre = reader.ReadElementContentAsString();
-
-                            if (!string.IsNullOrWhiteSpace(genre))
+                            var genre = reader.ReadNormalizedString();
+                            if (!string.IsNullOrEmpty(genre))
                             {
                             {
                                 item.AddGenre(genre);
                                 item.AddGenre(genre);
                             }
                             }
 
 
                             break;
                             break;
-                        }
-
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
                             break;
                             break;
@@ -885,17 +652,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     switch (reader.Name)
                     switch (reader.Name)
                     {
                     {
                         case "Tag":
                         case "Tag":
-                        {
-                            var tag = reader.ReadElementContentAsString();
-
-                            if (!string.IsNullOrWhiteSpace(tag))
+                            var tag = reader.ReadNormalizedString();
+                            if (!string.IsNullOrEmpty(tag))
                             {
                             {
                                 tags.Add(tag);
                                 tags.Add(tag);
                             }
                             }
 
 
                             break;
                             break;
-                        }
-
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
                             break;
                             break;
@@ -929,29 +692,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     {
                     {
                         case "Person":
                         case "Person":
                         case "Actor":
                         case "Actor":
-                        {
-                            if (reader.IsEmptyElement)
+                            var person = reader.GetPersonFromXmlNode();
+                            if (person is not null)
                             {
                             {
-                                reader.Read();
-                                continue;
-                            }
-
-                            using (var subtree = reader.ReadSubtree())
-                            {
-                                foreach (var person in GetPersonsFromXmlNode(subtree))
-                                {
-                                    if (string.IsNullOrWhiteSpace(person.Name))
-                                    {
-                                        continue;
-                                    }
-
-                                    item.AddPerson(person);
-                                }
+                                item.AddPerson(person);
                             }
                             }
 
 
                             break;
                             break;
-                        }
-
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
                             break;
                             break;
@@ -977,17 +724,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     switch (reader.Name)
                     switch (reader.Name)
                     {
                     {
                         case "Trailer":
                         case "Trailer":
-                        {
-                            var val = reader.ReadElementContentAsString();
-
-                            if (!string.IsNullOrWhiteSpace(val))
+                            var trailer = reader.ReadNormalizedString();
+                            if (!string.IsNullOrEmpty(trailer))
                             {
                             {
-                                item.AddTrailerUrl(val);
+                                item.AddTrailerUrl(trailer);
                             }
                             }
 
 
                             break;
                             break;
-                        }
-
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
                             break;
                             break;
@@ -1018,17 +761,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     switch (reader.Name)
                     switch (reader.Name)
                     {
                     {
                         case "Studio":
                         case "Studio":
-                        {
-                            var studio = reader.ReadElementContentAsString();
-
-                            if (!string.IsNullOrWhiteSpace(studio))
+                            var studio = reader.ReadNormalizedString();
+                            if (!string.IsNullOrEmpty(studio))
                             {
                             {
                                 item.AddStudio(studio);
                                 item.AddStudio(studio);
                             }
                             }
 
 
                             break;
                             break;
-                        }
-
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
                             break;
                             break;
@@ -1041,83 +780,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Gets the persons from XML node.
-        /// </summary>
-        /// <param name="reader">The reader.</param>
-        /// <returns>IEnumerable{PersonInfo}.</returns>
-        private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
-        {
-            var name = string.Empty;
-            var type = PersonKind.Actor; // If type is not specified assume actor
-            var role = string.Empty;
-            int? sortOrder = null;
-
-            reader.MoveToContent();
-            reader.Read();
-
-            // Loop through each element
-            while (!reader.EOF && reader.ReadState == ReadState.Interactive)
-            {
-                if (reader.NodeType == XmlNodeType.Element)
-                {
-                    switch (reader.Name)
-                    {
-                        case "Name":
-                            name = reader.ReadElementContentAsString();
-                            break;
-
-                        case "Type":
-                        {
-                            var val = reader.ReadElementContentAsString();
-                            _ = Enum.TryParse(val, true, out type);
-
-                            break;
-                        }
-
-                        case "Role":
-                        {
-                            var val = reader.ReadElementContentAsString();
-
-                            if (!string.IsNullOrWhiteSpace(val))
-                            {
-                                role = val;
-                            }
-
-                            break;
-                        }
-
-                        case "SortOrder":
-                        {
-                            var val = reader.ReadElementContentAsString();
-
-                            if (!string.IsNullOrWhiteSpace(val))
-                            {
-                                if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
-                                {
-                                    sortOrder = intVal;
-                                }
-                            }
-
-                            break;
-                        }
-
-                        default:
-                            reader.Skip();
-                            break;
-                    }
-                }
-                else
-                {
-                    reader.Read();
-                }
-            }
-
-            var personInfo = new PersonInfo { Name = name.Trim(), Role = role, Type = type, SortOrder = sortOrder };
-
-            return new[] { personInfo };
-        }
-
         /// <summary>
         /// <summary>
         /// Get linked child.
         /// Get linked child.
         /// </summary>
         /// </summary>
@@ -1138,17 +800,11 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     switch (reader.Name)
                     switch (reader.Name)
                     {
                     {
                         case "Path":
                         case "Path":
-                        {
-                            linkedItem.Path = reader.ReadElementContentAsString();
+                            linkedItem.Path = reader.ReadNormalizedString();
                             break;
                             break;
-                        }
-
                         case "ItemId":
                         case "ItemId":
-                        {
-                            linkedItem.LibraryItemId = reader.ReadElementContentAsString();
+                            linkedItem.LibraryItemId = reader.ReadNormalizedString();
                             break;
                             break;
-                        }
-
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
                             break;
                             break;
@@ -1189,22 +845,14 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     switch (reader.Name)
                     switch (reader.Name)
                     {
                     {
                         case "UserId":
                         case "UserId":
-                        {
-                            item.UserId = reader.ReadElementContentAsString();
+                            item.UserId = reader.ReadNormalizedString();
                             break;
                             break;
-                        }
-
                         case "CanEdit":
                         case "CanEdit":
-                        {
                             item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
                             item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
                             break;
                             break;
-                        }
-
                         default:
                         default:
-                        {
                             reader.Skip();
                             reader.Skip();
                             break;
                             break;
-                        }
                     }
                     }
                 }
                 }
                 else
                 else
@@ -1221,34 +869,5 @@ namespace MediaBrowser.LocalMetadata.Parsers
 
 
             return null;
             return null;
         }
         }
-
-        /// <summary>
-        /// Used to split names of comma or pipe delimited genres and people.
-        /// </summary>
-        /// <param name="value">The value.</param>
-        /// <returns>IEnumerable{System.String}.</returns>
-        private IEnumerable<string> SplitNames(string value)
-        {
-            // Only split by comma if there is no pipe in the string
-            // We have to be careful to not split names like Matthew, Jr.
-            var separator = !value.Contains('|', StringComparison.Ordinal)
-                            && !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' };
-
-            value = value.Trim().Trim(separator);
-
-            return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : Split(value, separator, StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        /// <summary>
-        /// Provides an additional overload for string.split.
-        /// </summary>
-        /// <param name="val">The val.</param>
-        /// <param name="separators">The separators.</param>
-        /// <param name="options">The options.</param>
-        /// <returns>System.String[][].</returns>
-        private string[] Split(string val, char[] separators, StringSplitOptions options)
-        {
-            return val.Split(separators, options);
-        }
     }
     }
 }
 }

+ 2 - 7
MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs

@@ -1,6 +1,7 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Xml;
 using System.Xml;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -30,12 +31,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
             switch (reader.Name)
             switch (reader.Name)
             {
             {
                 case "PlaylistMediaType":
                 case "PlaylistMediaType":
-                {
-                    item.PlaylistMediaType = reader.ReadElementContentAsString();
-
+                    item.PlaylistMediaType = reader.ReadNormalizedString();
                     break;
                     break;
-                }
-
                 case "PlaylistItems":
                 case "PlaylistItems":
 
 
                     if (!reader.IsEmptyElement)
                     if (!reader.IsEmptyElement)
@@ -94,10 +91,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
                         }
                         }
 
 
                         default:
                         default:
-                        {
                             reader.Skip();
                             reader.Skip();
                             break;
                             break;
-                        }
                     }
                     }
                 }
                 }
                 else
                 else

+ 20 - 60
MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs

@@ -1,4 +1,3 @@
-#nullable disable
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging;
 
 
 namespace MediaBrowser.MediaEncoding.Attachments
 namespace MediaBrowser.MediaEncoding.Attachments
 {
 {
-    public class AttachmentExtractor : IAttachmentExtractor, IDisposable
+    public sealed class AttachmentExtractor : IAttachmentExtractor
     {
     {
         private readonly ILogger<AttachmentExtractor> _logger;
         private readonly ILogger<AttachmentExtractor> _logger;
         private readonly IApplicationPaths _appPaths;
         private readonly IApplicationPaths _appPaths;
@@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
         private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
         private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
             new ConcurrentDictionary<string, SemaphoreSlim>();
             new ConcurrentDictionary<string, SemaphoreSlim>();
 
 
-        private bool _disposed = false;
-
         public AttachmentExtractor(
         public AttachmentExtractor(
             ILogger<AttachmentExtractor> logger,
             ILogger<AttachmentExtractor> logger,
             IApplicationPaths appPaths,
             IApplicationPaths appPaths,
@@ -177,22 +174,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
 
 
                 process.Start();
                 process.Start();
 
 
-                var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
-
-                if (!ranToCompletion)
+                try
                 {
                 {
-                    try
-                    {
-                        _logger.LogWarning("Killing ffmpeg attachment extraction process");
-                        process.Kill();
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error killing attachment extraction process");
-                    }
+                    await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+                    exitCode = process.ExitCode;
+                }
+                catch (OperationCanceledException)
+                {
+                    process.Kill(true);
+                    exitCode = -1;
                 }
                 }
-
-                exitCode = ranToCompletion ? process.ExitCode : -1;
             }
             }
 
 
             var failed = false;
             var failed = false;
@@ -296,7 +287,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
 
 
             ArgumentException.ThrowIfNullOrEmpty(outputPath);
             ArgumentException.ThrowIfNullOrEmpty(outputPath);
 
 
-            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+            Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
 
 
             var processArgs = string.Format(
             var processArgs = string.Format(
                 CultureInfo.InvariantCulture,
                 CultureInfo.InvariantCulture,
@@ -325,22 +316,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
 
 
                 process.Start();
                 process.Start();
 
 
-                var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
-
-                if (!ranToCompletion)
+                try
                 {
                 {
-                    try
-                    {
-                        _logger.LogWarning("Killing ffmpeg attachment extraction process");
-                        process.Kill();
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error killing attachment extraction process");
-                    }
+                    await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+                    exitCode = process.ExitCode;
+                }
+                catch (OperationCanceledException)
+                {
+                    process.Kill(true);
+                    exitCode = -1;
                 }
                 }
-
-                exitCode = ranToCompletion ? process.ExitCode : -1;
             }
             }
 
 
             var failed = false;
             var failed = false;
@@ -391,33 +376,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
                 filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
                 filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
             }
             }
 
 
-            var prefix = filename.Substring(0, 1);
-            return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename);
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-            }
-
-            _disposed = true;
+            var prefix = filename.AsSpan(0, 1);
+            return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
         }
         }
     }
     }
 }
 }

+ 26 - 26
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -316,10 +316,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
             {
             {
                 var files = _fileSystem.GetFilePaths(path, recursive);
                 var files = _fileSystem.GetFilePaths(path, recursive);
 
 
-                var excludeExtensions = new[] { ".c" };
-
-                return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
-                                                    && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
+                return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase)
+                                                    && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase));
             }
             }
             catch (Exception)
             catch (Exception)
             {
             {
@@ -419,6 +417,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
             var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
             var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
             var analyzeDuration = string.Empty;
             var analyzeDuration = string.Empty;
             var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
             var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
+            var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
+            var extraArgs = string.Empty;
 
 
             if (request.MediaSource.AnalyzeDurationMs > 0)
             if (request.MediaSource.AnalyzeDurationMs > 0)
             {
             {
@@ -429,12 +429,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
                 analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
             }
             }
 
 
+            if (!string.IsNullOrEmpty(analyzeDuration))
+            {
+                extraArgs = analyzeDuration;
+            }
+
+            if (!string.IsNullOrEmpty(ffmpegProbeSize))
+            {
+                extraArgs += " -probesize " + ffmpegProbeSize;
+            }
+
             return GetMediaInfoInternal(
             return GetMediaInfoInternal(
                 GetInputArgument(request.MediaSource.Path, request.MediaSource),
                 GetInputArgument(request.MediaSource.Path, request.MediaSource),
                 request.MediaSource.Path,
                 request.MediaSource.Path,
                 request.MediaSource.Protocol,
                 request.MediaSource.Protocol,
                 extractChapters,
                 extractChapters,
-                analyzeDuration,
+                extraArgs,
                 request.MediaType == DlnaProfileType.Audio,
                 request.MediaType == DlnaProfileType.Audio,
                 request.MediaSource.VideoType,
                 request.MediaSource.VideoType,
                 cancellationToken);
                 cancellationToken);
@@ -640,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         {
         {
             ArgumentException.ThrowIfNullOrEmpty(inputPath);
             ArgumentException.ThrowIfNullOrEmpty(inputPath);
 
 
-            var outputExtension = targetFormat switch
-            {
-                ImageFormat.Bmp => ".bmp",
-                ImageFormat.Gif => ".gif",
-                ImageFormat.Jpg => ".jpg",
-                ImageFormat.Png => ".png",
-                ImageFormat.Webp => ".webp",
-                _ => ".jpg"
-            };
+            var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
 
 
             var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
             var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
             Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
             Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
@@ -750,11 +752,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
                         timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout;
                         timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout;
                     }
                     }
 
 
-                    ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
-
-                    if (!ranToCompletion)
+                    try
+                    {
+                        await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
+                        ranToCompletion = true;
+                    }
+                    catch (OperationCanceledException)
                     {
                     {
-                        StopProcess(processWrapper, 1000);
+                        process.Kill(true);
+                        ranToCompletion = false;
                     }
                     }
                 }
                 }
                 finally
                 finally
@@ -989,7 +995,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return true;
             return true;
         }
         }
 
 
-        private class ProcessWrapper : IDisposable
+        private sealed class ProcessWrapper : IDisposable
         {
         {
             private readonly MediaEncoder _mediaEncoder;
             private readonly MediaEncoder _mediaEncoder;
 
 
@@ -1032,13 +1038,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     _mediaEncoder._runningProcesses.Remove(this);
                     _mediaEncoder._runningProcesses.Remove(this);
                 }
                 }
 
 
-                try
-                {
-                    process.Dispose();
-                }
-                catch
-                {
-                }
+                process.Dispose();
             }
             }
 
 
             public void Dispose()
             public void Dispose()

+ 1 - 0
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -78,6 +78,7 @@ namespace MediaBrowser.MediaEncoding.Probing
             "She/Her/Hers",
             "She/Her/Hers",
             "5/8erl in Ehr'n",
             "5/8erl in Ehr'n",
             "Smith/Kotzen",
             "Smith/Kotzen",
+            "We;Na",
         };
         };
 
 
         /// <summary>
         /// <summary>

+ 16 - 30
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -420,23 +420,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     throw;
                     throw;
                 }
                 }
 
 
-                var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
-
-                if (!ranToCompletion)
+                try
                 {
                 {
-                    try
-                    {
-                        _logger.LogInformation("Killing ffmpeg subtitle conversion process");
-
-                        process.Kill();
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error killing subtitle conversion process");
-                    }
+                    await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
+                    exitCode = process.ExitCode;
+                }
+                catch (OperationCanceledException)
+                {
+                    process.Kill(true);
+                    exitCode = -1;
                 }
                 }
-
-                exitCode = ranToCompletion ? process.ExitCode : -1;
             }
             }
 
 
             var failed = false;
             var failed = false;
@@ -574,23 +567,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                     throw;
                     throw;
                 }
                 }
 
 
-                var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
-
-                if (!ranToCompletion)
+                try
                 {
                 {
-                    try
-                    {
-                        _logger.LogWarning("Killing ffmpeg subtitle extraction process");
-
-                        process.Kill();
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error killing subtitle extraction process");
-                    }
+                    await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
+                    exitCode = process.ExitCode;
+                }
+                catch (OperationCanceledException)
+                {
+                    process.Kill(true);
+                    exitCode = -1;
                 }
                 }
-
-                exitCode = ranToCompletion ? process.ExitCode : -1;
             }
             }
 
 
             var failed = false;
             var failed = false;

+ 17 - 0
MediaBrowser.Model/Drawing/ImageFormatExtensions.cs

@@ -24,4 +24,21 @@ public static class ImageFormatExtensions
             ImageFormat.Webp => "image/webp",
             ImageFormat.Webp => "image/webp",
             _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
             _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
         };
         };
+
+    /// <summary>
+    /// Returns the correct extension for this <see cref="ImageFormat" />.
+    /// </summary>
+    /// <param name="format">This <see cref="ImageFormat" />.</param>
+    /// <exception cref="InvalidEnumArgumentException">The <paramref name="format"/> is an invalid enumeration value.</exception>
+    /// <returns>The correct extension for this <see cref="ImageFormat" />.</returns>
+    public static string GetExtension(this ImageFormat format)
+        => format switch
+        {
+            ImageFormat.Bmp => ".bmp",
+            ImageFormat.Gif => ".gif",
+            ImageFormat.Jpg => ".jpg",
+            ImageFormat.Png => ".png",
+            ImageFormat.Webp => ".webp",
+            _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
+        };
 }
 }

+ 0 - 2
MediaBrowser.Model/IO/IFileSystem.cs

@@ -10,8 +10,6 @@ namespace MediaBrowser.Model.IO
     /// </summary>
     /// </summary>
     public interface IFileSystem
     public interface IFileSystem
     {
     {
-        void AddShortcutHandler(IShortcutHandler handler);
-
         /// <summary>
         /// <summary>
         /// Determines whether the specified filename is shortcut.
         /// Determines whether the specified filename is shortcut.
         /// </summary>
         /// </summary>

+ 5 - 1
MediaBrowser.Providers/Manager/ImageSaver.cs

@@ -263,7 +263,11 @@ namespace MediaBrowser.Providers.Manager
 
 
                 var fileStreamOptions = AsyncFile.WriteOptions;
                 var fileStreamOptions = AsyncFile.WriteOptions;
                 fileStreamOptions.Mode = FileMode.Create;
                 fileStreamOptions.Mode = FileMode.Create;
-                fileStreamOptions.PreallocationSize = source.Length;
+                if (source.CanSeek)
+                {
+                    fileStreamOptions.PreallocationSize = source.Length;
+                }
+
                 var fs = new FileStream(path, fileStreamOptions);
                 var fs = new FileStream(path, fileStreamOptions);
                 await using (fs.ConfigureAwait(false))
                 await using (fs.ConfigureAwait(false))
                 {
                 {

+ 0 - 6
MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs

@@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo
                 ? Path.GetExtension(attachmentStream.FileName)
                 ? Path.GetExtension(attachmentStream.FileName)
                 : MimeTypes.ToExtension(attachmentStream.MimeType);
                 : MimeTypes.ToExtension(attachmentStream.MimeType);
 
 
-            if (string.IsNullOrEmpty(extension))
-            {
-                extension = ".jpg";
-            }
-
             ImageFormat format = extension switch
             ImageFormat format = extension switch
             {
             {
                 ".bmp" => ImageFormat.Bmp,
                 ".bmp" => ImageFormat.Bmp,
                 ".gif" => ImageFormat.Gif,
                 ".gif" => ImageFormat.Gif,
-                ".jpg" => ImageFormat.Jpg,
                 ".png" => ImageFormat.Png,
                 ".png" => ImageFormat.Png,
                 ".webp" => ImageFormat.Webp,
                 ".webp" => ImageFormat.Webp,
                 _ => ImageFormat.Jpg
                 _ => ImageFormat.Jpg

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 245 - 606
MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs


+ 61 - 114
MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs

@@ -1,10 +1,11 @@
 using System;
 using System;
-using System.Globalization;
 using System.IO;
 using System.IO;
+using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Xml;
 using System.Xml;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                 }
                 }
 
 
                 // Extract the last episode number from nfo
                 // Extract the last episode number from nfo
+                // Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode
                 // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
                 // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
+                var name = new StringBuilder(item.Item.Name);
+                var overview = new StringBuilder(item.Item.Overview);
                 while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
                 while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
                 {
                 {
                     xml = xmlFile.Substring(0, index + srch.Length);
                     xml = xmlFile.Substring(0, index + srch.Length);
@@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                     {
                     {
                         reader.MoveToContent();
                         reader.MoveToContent();
 
 
-                        if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num))
+                        while (!reader.EOF && reader.ReadState == ReadState.Interactive)
                         {
                         {
-                            item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
+                            cancellationToken.ThrowIfCancellationRequested();
+
+                            if (reader.NodeType == XmlNodeType.Element)
+                            {
+                                switch (reader.Name)
+                                {
+                                    case "name":
+                                    case "title":
+                                    case "localtitle":
+                                        name.Append(" / ").Append(reader.ReadElementContentAsString());
+                                        break;
+                                    case "episode":
+                                        {
+                                            if (int.TryParse(reader.ReadElementContentAsString(), out var num))
+                                            {
+                                                item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
+                                            }
+
+                                            break;
+                                        }
+
+                                    case "biography":
+                                    case "plot":
+                                    case "review":
+                                        overview.Append(" / ").Append(reader.ReadElementContentAsString());
+                                        break;
+                                }
+                            }
+
+                            reader.Read();
                         }
                         }
                     }
                     }
                 }
                 }
+
+                item.Item.Name = name.ToString();
+                item.Item.Overview = overview.ToString();
             }
             }
             catch (XmlException)
             catch (XmlException)
             {
             {
@@ -112,142 +148,53 @@ namespace MediaBrowser.XbmcMetadata.Parsers
             switch (reader.Name)
             switch (reader.Name)
             {
             {
                 case "season":
                 case "season":
+                    if (reader.TryReadInt(out var seasonNumber))
                     {
                     {
-                        var number = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(number))
-                        {
-                            if (int.TryParse(number, out var num))
-                            {
-                                item.ParentIndexNumber = num;
-                            }
-                        }
-
-                        break;
+                        item.ParentIndexNumber = seasonNumber;
                     }
                     }
 
 
+                    break;
                 case "episode":
                 case "episode":
+                    if (reader.TryReadInt(out var episodeNumber))
                     {
                     {
-                        var number = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(number))
-                        {
-                            if (int.TryParse(number, out var num))
-                            {
-                                item.IndexNumber = num;
-                            }
-                        }
-
-                        break;
+                        item.IndexNumber = episodeNumber;
                     }
                     }
 
 
+                    break;
                 case "episodenumberend":
                 case "episodenumberend":
+                    if (reader.TryReadInt(out var episodeNumberEnd))
                     {
                     {
-                        var number = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(number))
-                        {
-                            if (int.TryParse(number, out var num))
-                            {
-                                item.IndexNumberEnd = num;
-                            }
-                        }
-
-                        break;
+                        item.IndexNumberEnd = episodeNumberEnd;
                     }
                     }
 
 
+                    break;
                 case "airsbefore_episode":
                 case "airsbefore_episode":
+                case "displayepisode":
+                    if (reader.TryReadInt(out var airsBeforeEpisode))
                     {
                     {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val))
-                        {
-                            // int.TryParse is local aware, so it can be problematic, force us culture
-                            if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
-                            {
-                                item.AirsBeforeEpisodeNumber = rval;
-                            }
-                        }
-
-                        break;
+                        item.AirsBeforeEpisodeNumber = airsBeforeEpisode;
                     }
                     }
 
 
+                    break;
                 case "airsafter_season":
                 case "airsafter_season":
+                case "displayafterseason":
+                    if (reader.TryReadInt(out var airsAfterSeason))
                     {
                     {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val))
-                        {
-                            // int.TryParse is local aware, so it can be problematic, force us culture
-                            if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
-                            {
-                                item.AirsAfterSeasonNumber = rval;
-                            }
-                        }
-
-                        break;
+                        item.AirsAfterSeasonNumber = airsAfterSeason;
                     }
                     }
 
 
+                    break;
                 case "airsbefore_season":
                 case "airsbefore_season":
-                    {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val))
-                        {
-                            // int.TryParse is local aware, so it can be problematic, force us culture
-                            if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
-                            {
-                                item.AirsBeforeSeasonNumber = rval;
-                            }
-                        }
-
-                        break;
-                    }
-
                 case "displayseason":
                 case "displayseason":
+                    if (reader.TryReadInt(out var airsBeforeSeason))
                     {
                     {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val))
-                        {
-                            // int.TryParse is local aware, so it can be problematic, force us culture
-                            if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
-                            {
-                                item.AirsBeforeSeasonNumber = rval;
-                            }
-                        }
-
-                        break;
-                    }
-
-                case "displayepisode":
-                    {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val))
-                        {
-                            // int.TryParse is local aware, so it can be problematic, force us culture
-                            if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
-                            {
-                                item.AirsBeforeEpisodeNumber = rval;
-                            }
-                        }
-
-                        break;
+                        item.AirsBeforeSeasonNumber = airsBeforeSeason;
                     }
                     }
 
 
+                    break;
                 case "showtitle":
                 case "showtitle":
-                    {
-                        var showtitle = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(showtitle))
-                        {
-                            item.SeriesName = showtitle;
-                        }
-
-                        break;
-                    }
-
+                    item.SeriesName = reader.ReadNormalizedString();
+                    break;
                 default:
                 default:
                     base.FetchDataFromXmlNode(reader, itemResult);
                     base.FetchDataFromXmlNode(reader, itemResult);
                     break;
                     break;

+ 11 - 18
MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs

@@ -5,6 +5,7 @@ using System.Xml;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
@@ -113,31 +114,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                     }
                     }
 
 
                 case "artist":
                 case "artist":
+                    var artist = reader.ReadNormalizedString();
+                    if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo)
                     {
                     {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
-                        {
-                            var list = movie.Artists.ToList();
-                            list.Add(val);
-                            movie.Artists = list.ToArray();
-                        }
-
-                        break;
+                        var list = artistVideo.Artists.ToList();
+                        list.Add(artist);
+                        artistVideo.Artists = list.ToArray();
                     }
                     }
 
 
+                    break;
                 case "album":
                 case "album":
+                    var album = reader.ReadNormalizedString();
+                    if (!string.IsNullOrEmpty(album) && item is MusicVideo albumVideo)
                     {
                     {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
-                        {
-                            movie.Album = val;
-                        }
-
-                        break;
+                        albumVideo.Album = album;
                     }
                     }
 
 
+                    break;
                 default:
                 default:
                     base.FetchDataFromXmlNode(reader, itemResult);
                     base.FetchDataFromXmlNode(reader, itemResult);
                     break;
                     break;

+ 6 - 23
MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs

@@ -1,7 +1,7 @@
-using System.Globalization;
 using System.Xml;
 using System.Xml;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -41,32 +41,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
             switch (reader.Name)
             switch (reader.Name)
             {
             {
                 case "seasonnumber":
                 case "seasonnumber":
+                    if (reader.TryReadInt(out var seasonNumber))
                     {
                     {
-                        var number = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(number))
-                        {
-                            if (int.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
-                            {
-                                item.IndexNumber = num;
-                            }
-                        }
-
-                        break;
+                        item.IndexNumber = seasonNumber;
                     }
                     }
 
 
+                    break;
                 case "seasonname":
                 case "seasonname":
-                    {
-                        var name = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(name))
-                        {
-                            item.Name = name;
-                        }
-
-                        break;
-                    }
-
+                    item.Name = reader.ReadNormalizedString();
+                    break;
                 default:
                 default:
                     base.FetchDataFromXmlNode(reader, itemResult);
                     base.FetchDataFromXmlNode(reader, itemResult);
                     break;
                     break;

+ 5 - 17
MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs

@@ -1,9 +1,9 @@
 using System;
 using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Xml;
 using System.Xml;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
@@ -76,23 +76,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                     }
                     }
 
 
                 case "airs_dayofweek":
                 case "airs_dayofweek":
-                    {
-                        item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
-                        break;
-                    }
-
+                    item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
+                    break;
                 case "airs_time":
                 case "airs_time":
-                    {
-                        var val = reader.ReadElementContentAsString();
-
-                        if (!string.IsNullOrWhiteSpace(val))
-                        {
-                            item.AirTime = val;
-                        }
-
-                        break;
-                    }
-
+                    item.AirTime = reader.ReadNormalizedString();
+                    break;
                 case "status":
                 case "status":
                     {
                     {
                         var status = reader.ReadElementContentAsString();
                         var status = reader.ReadElementContentAsString();

+ 2 - 2
MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs

@@ -60,13 +60,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
             }
             }
             else
             else
             {
             {
-                yield return Path.ChangeExtension(item.Path, ".nfo");
-
                 // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
                 // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
                 if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
                 if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
                 {
                 {
                     yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
                     yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
                 }
                 }
+
+                yield return Path.ChangeExtension(item.Path, ".nfo");
             }
             }
         }
         }
 
 

+ 1 - 0
debian/jellyfin.init

@@ -1,3 +1,4 @@
+#!/bin/sh
 ### BEGIN INIT INFO
 ### BEGIN INIT INFO
 # Provides:          Jellyfin Media Server
 # Provides:          Jellyfin Media Server
 # Required-Start:    $local_fs $network
 # Required-Start:    $local_fs $network

+ 1 - 1
fedora/jellyfin.service

@@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin
 User = jellyfin
 User = jellyfin
 Group = jellyfin
 Group = jellyfin
 WorkingDirectory = /var/lib/jellyfin
 WorkingDirectory = /var/lib/jellyfin
-ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
+ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
 Restart = on-failure
 Restart = on-failure
 TimeoutSec = 15
 TimeoutSec = 15
 SuccessExitStatus=0 143
 SuccessExitStatus=0 143

+ 1 - 0
fedora/jellyfin.spec

@@ -75,6 +75,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
 %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
 %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
 %{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
 %{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
 %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin
 %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin
+sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin
 
 
 # Jellyfin config
 # Jellyfin config
 %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
 %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json

+ 3 - 13
src/Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -188,7 +188,7 @@ public class SkiaEncoder : IImageEncoder
             return path;
             return path;
         }
         }
 
 
-        var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
+        var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan())));
         var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
         var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
         Directory.CreateDirectory(directory);
         Directory.CreateDirectory(directory);
         File.Copy(path, tempPath, true);
         File.Copy(path, tempPath, true);
@@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder
     {
     {
         if (!orientation.HasValue)
         if (!orientation.HasValue)
         {
         {
-            return SKEncodedOrigin.TopLeft;
+            return SKEncodedOrigin.Default;
         }
         }
 
 
-        return orientation.Value switch
-        {
-            ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
-            ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
-            ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
-            ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
-            ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
-            ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
-            ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
-            _ => SKEncodedOrigin.TopLeft
-        };
+        return (SKEncodedOrigin)orientation.Value;
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 6 - 6
src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs

@@ -38,25 +38,25 @@ public partial class StripCollageBuilder
     {
     {
         ArgumentNullException.ThrowIfNull(outputPath);
         ArgumentNullException.ThrowIfNull(outputPath);
 
 
-        var ext = Path.GetExtension(outputPath);
+        var ext = Path.GetExtension(outputPath.AsSpan());
 
 
-        if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
-            || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
+        if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
+            || ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
         {
         {
             return SKEncodedImageFormat.Jpeg;
             return SKEncodedImageFormat.Jpeg;
         }
         }
 
 
-        if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
+        if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase))
         {
         {
             return SKEncodedImageFormat.Webp;
             return SKEncodedImageFormat.Webp;
         }
         }
 
 
-        if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
+        if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase))
         {
         {
             return SKEncodedImageFormat.Gif;
             return SKEncodedImageFormat.Gif;
         }
         }
 
 
-        if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
+        if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase))
         {
         {
             return SKEncodedImageFormat.Bmp;
             return SKEncodedImageFormat.Bmp;
         }
         }

+ 2 - 54
src/Jellyfin.Drawing/ImageProcessor.cs

@@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
     /// <inheritdoc />
     /// <inheritdoc />
     public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
     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 />
     /// <inheritdoc />
     public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
     public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
         => _imageEncoder.SupportedOutputFormats;
         => _imageEncoder.SupportedOutputFormats;
 
 
-    /// <inheritdoc />
-    public bool SupportsTransparency(string path)
-        => _transparentImageTypes.Contains(Path.GetExtension(path));
-
     /// <inheritdoc />
     /// <inheritdoc />
     public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
     public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
     {
     {
@@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
                 }
                 }
             }
             }
 
 
-            return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
+            return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
         }
         }
         catch (Exception ex)
         catch (Exception ex)
         {
         {
@@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
         return ImageFormat.Jpg;
         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>
     /// <summary>
     /// Gets the cache file path based on a set of parameters.
     /// Gets the cache file path based on a set of parameters.
     /// </summary>
     /// </summary>
@@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
         filename.Append(",v=");
         filename.Append(",v=");
         filename.Append(Version);
         filename.Append(Version);
 
 
-        return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
+        return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension());
     }
     }
 
 
     /// <inheritdoc />
     /// <inheritdoc />
@@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
             return Task.FromResult((originalImagePath, dateModified));
             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));
         return Task.FromResult((originalImagePath, dateModified));
     }
     }
 
 

+ 13 - 0
tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs

@@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests
     [InlineData((ImageFormat)5)]
     [InlineData((ImageFormat)5)]
     public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
     public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
         => Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
         => Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
+
+    [Theory]
+    [MemberData(nameof(GetAllImageFormats))]
+    public static void GetExtension_Valid_Valid(ImageFormat format)
+        => Assert.Null(Record.Exception(() => format.GetExtension()));
+
+    [Theory]
+    [InlineData((ImageFormat)int.MinValue)]
+    [InlineData((ImageFormat)int.MaxValue)]
+    [InlineData((ImageFormat)(-1))]
+    [InlineData((ImageFormat)5)]
+    public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
+        => Assert.Throws<InvalidEnumArgumentException>(() => format.GetExtension());
 }
 }

+ 1 - 0
tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs

@@ -31,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
         [InlineData("/media/music/Foo B.A.R./epic.flac", false)]
         [InlineData("/media/music/Foo B.A.R./epic.flac", false)]
         [InlineData("/media/music/Foo B.A.R", false)]
         [InlineData("/media/music/Foo B.A.R", false)]
         [InlineData("/media/music/Foo B.A.R.", false)]
         [InlineData("/media/music/Foo B.A.R.", false)]
+        [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
         public void PathIgnored(string path, bool expected)
         public void PathIgnored(string path, bool expected)
         {
         {
             Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
             Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));

+ 8 - 5
tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs

@@ -15,8 +15,8 @@ namespace Jellyfin.Server.Integration.Tests
 {
 {
     public static class AuthHelper
     public static class AuthHelper
     {
     {
-        public const string AuthHeaderName = "X-Emby-Authorization";
-        public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\"";
+        public const string AuthHeaderName = "Authorization";
+        public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server%20Integration%20Tests\", DeviceId=\"69420\", Device=\"Apple%20II\", Version=\"10.8.0\"";
 
 
         public static async Task<string> CompleteStartupAsync(HttpClient client)
         public static async Task<string> CompleteStartupAsync(HttpClient client)
         {
         {
@@ -27,16 +27,19 @@ namespace Jellyfin.Server.Integration.Tests
             using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
             using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
             Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
             Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
 
 
-            using var content = JsonContent.Create(
+            using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/Users/AuthenticateByName");
+            httpRequest.Headers.TryAddWithoutValidation(AuthHeaderName, DummyAuthHeader);
+            httpRequest.Content = JsonContent.Create(
                 new AuthenticateUserByName()
                 new AuthenticateUserByName()
                 {
                 {
                     Username = user!.Name,
                     Username = user!.Name,
                     Pw = user.Password,
                     Pw = user.Password,
                 },
                 },
                 options: jsonOptions);
                 options: jsonOptions);
-            content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
 
 
-            using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content);
+            using var authResponse = await client.SendAsync(httpRequest);
+            authResponse.EnsureSuccessStatusCode();
+
             var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
             var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
                 await authResponse.Content.ReadAsStreamAsync(),
                 await authResponse.Content.ReadAsStreamAsync(),
                 jsonOptions);
                 jsonOptions);

+ 26 - 0
tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs

@@ -0,0 +1,26 @@
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public class PersonsControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+    private readonly JellyfinApplicationFactory _factory;
+    private static string? _accessToken;
+
+    public PersonsControllerTests(JellyfinApplicationFactory factory)
+    {
+        _factory = factory;
+    }
+
+    [Fact]
+    public async Task GetPerson_DoesntExist_NotFound()
+    {
+        var client = _factory.CreateClient();
+        client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+        using var response = await client.GetAsync($"Persons/DoesntExist");
+        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+    }
+}

+ 1 - 2
tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs

@@ -2,7 +2,6 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
-using System.Threading;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Helpers;
 using Jellyfin.Server.Helpers;
@@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests
             var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
             var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
             appHost.ServiceProvider = testServer.Services;
             appHost.ServiceProvider = testServer.Services;
             appHost.InitializeServices().GetAwaiter().GetResult();
             appHost.InitializeServices().GetAwaiter().GetResult();
-            appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult();
+            appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
 
 
             return testServer;
             return testServer;
         }
         }

+ 3 - 3
tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
@@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
             _parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
             _parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
 
 
             var item = result.Item;
             var item = result.Item;
-            Assert.Equal("Rising (1)", item.Name);
+            Assert.Equal("Rising (1) / Rising (2)", item.Name);
             Assert.Equal(1, item.IndexNumber);
             Assert.Equal(1, item.IndexNumber);
             Assert.Equal(2, item.IndexNumberEnd);
             Assert.Equal(2, item.IndexNumberEnd);
             Assert.Equal(1, item.ParentIndexNumber);
             Assert.Equal(1, item.ParentIndexNumber);
-            Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview);
+            Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. / Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.", item.Overview);
             Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
             Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
             Assert.Equal(2004, item.ProductionYear);
             Assert.Equal(2004, item.ProductionYear);
         }
         }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است