Преглед изворни кода

Merge pull request #6177 from Bond-009/async

Use async FileStreams where it makes sense
Claus Vium пре 3 година
родитељ
комит
74fef6c05b
40 измењених фајлова са 127 додато и 100 уклоњено
  1. 1 1
      Emby.Dlna/DlnaManager.cs
  2. 1 1
      Emby.Drawing/ImageProcessor.cs
  3. 2 2
      Emby.Server.Implementations/Channels/ChannelManager.cs
  4. 1 1
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  5. 3 2
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  6. 1 1
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  7. 2 2
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  8. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  9. 1 1
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  10. 2 2
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  11. 1 3
      Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
  12. 2 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  13. 32 30
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  14. 2 1
      Emby.Server.Implementations/Plugins/PluginManager.cs
  15. 1 1
      Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
  16. 1 1
      Jellyfin.Api/Controllers/ImageByNameController.cs
  17. 1 1
      Jellyfin.Api/Controllers/RemoteImageController.cs
  18. 1 1
      Jellyfin.Api/Controllers/SystemController.cs
  19. 1 1
      Jellyfin.Api/Helpers/HlsHelpers.cs
  20. 1 2
      Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
  21. 1 1
      Jellyfin.Api/Helpers/ProgressiveFileStream.cs
  22. 1 1
      Jellyfin.Api/Helpers/TranscodingJobHelper.cs
  23. 3 2
      Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
  24. 2 1
      Jellyfin.Server/Program.cs
  25. 1 1
      MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
  26. 3 3
      MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
  27. 1 1
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  28. 4 4
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  29. 34 0
      MediaBrowser.Model/IO/AsyncFile.cs
  30. 1 1
      MediaBrowser.Providers/Manager/ImageSaver.cs
  31. 1 1
      MediaBrowser.Providers/Manager/ItemImageProvider.cs
  32. 1 1
      MediaBrowser.Providers/Manager/ProviderManager.cs
  33. 2 1
      MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
  34. 2 2
      MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
  35. 2 1
      MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
  36. 2 2
      MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
  37. 4 18
      MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
  38. 1 1
      MediaBrowser.Providers/Studios/StudiosImageProvider.cs
  39. 1 1
      MediaBrowser.Providers/Subtitles/SubtitleManager.cs
  40. 2 1
      tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs

+ 1 - 1
Emby.Dlna/DlnaManager.cs

@@ -366,7 +366,7 @@ namespace Emby.Dlna
                         Directory.CreateDirectory(systemProfilesPath);
 
                         // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-                        using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
+                        using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
                         {
                             await stream.CopyToAsync(fileStream).ConfigureAwait(false);
                         }

+ 1 - 1
Emby.Drawing/ImageProcessor.cs

@@ -102,7 +102,7 @@ namespace Emby.Drawing
         {
             var file = await ProcessImage(options).ConfigureAwait(false);
 
-            using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
+            using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
             {
                 await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
             }

+ 2 - 2
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -815,7 +815,7 @@ namespace Emby.Server.Implementations.Channels
             {
                 if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                 {
-                    await using FileStream jsonStream = File.OpenRead(cachePath);
+                    await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
                     var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
                     if (cachedResult != null)
                     {
@@ -838,7 +838,7 @@ namespace Emby.Server.Implementations.Channels
                 {
                     if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                     {
-                        await using FileStream jsonStream = File.OpenRead(cachePath);
+                        await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
                         var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
                         if (cachedResult != null)
                         {

+ 1 - 1
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -248,7 +248,7 @@ namespace Emby.Server.Implementations.IO
                     {
                         try
                         {
-                            using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+                            using (Stream thisFileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1))
                             {
                                 result.Length = thisFileStream.Length;
                             }

+ 3 - 2
Emby.Server.Implementations/Library/LiveStreamHelper.cs

@@ -17,6 +17,7 @@ using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using Microsoft.Extensions.Logging;
 
@@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Library
             {
                 try
                 {
-                    await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+                    await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
                     mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
                     // _logger.LogDebug("Found cached media info");
@@ -86,7 +87,7 @@ namespace Emby.Server.Implementations.Library
                 if (cacheFilePath != null)
                 {
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
-                    await using FileStream createStream = File.OpenWrite(cacheFilePath);
+                    await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
                     await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
                     // _logger.LogDebug("Saved media info to {0}", cacheFilePath);

+ 1 - 1
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -638,7 +638,7 @@ namespace Emby.Server.Implementations.Library
             {
                 try
                 {
-                    await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+                    await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
                     mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
                     // _logger.LogDebug("Found cached media info");

+ 2 - 2
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
 
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-            using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
+            using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
             {
                 onStarted();
 
@@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
 
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-            await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
+            await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO);
 
             onStarted();
 

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
 
             // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
-            _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+            _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
 
             await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
             await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);

+ 1 - 1
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
+            await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO))
             {
                 await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
             }

+ 2 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                         try
                         {
                             Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
-                            await using var writeStream = File.OpenWrite(channelCacheFile);
+                            await using var writeStream = AsyncFile.OpenWrite(channelCacheFile);
                             await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
                         }
                         catch (IOException)
@@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                     {
                         try
                         {
-                            await using var readStream = File.OpenRead(channelCacheFile);
+                            await using var readStream = AsyncFile.OpenRead(channelCacheFile);
                             var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
                                 .ConfigureAwait(false);
                             list.AddRange(channels);

+ 1 - 3
Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs

@@ -155,15 +155,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
             cancellationToken = linkedCancellationTokenSource.Token;
 
-            // use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039
-            var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT;
-
             bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
 
             var nextFileInfo = GetNextFile(null);
             var nextFile = nextFileInfo.file;
             var isLastFile = nextFileInfo.isLastFile;
 
+            var allowAsync = AsyncFile.UseAsyncIO;
             while (!string.IsNullOrEmpty(nextFile))
             {
                 var emptyReadLimit = isLastFile ? EmptyReadLimit : 1;

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

@@ -14,6 +14,7 @@ using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using Microsoft.Extensions.Logging;
 
@@ -50,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
             if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             {
-                return File.OpenRead(info.Url);
+                return AsyncFile.OpenRead(info.Url);
             }
 
             using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);

+ 32 - 30
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -129,37 +129,39 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
         {
-            return Task.Run(async () =>
-            {
-                try
-                {
-                    Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
-                    using var message = response;
-                    await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-                    await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
-                    await StreamHelper.CopyToAsync(
-                        stream,
-                        fileStream,
-                        IODefaults.CopyToBufferSize,
-                        () => Resolve(openTaskCompletionSource),
-                        cancellationToken).ConfigureAwait(false);
-                }
-                catch (OperationCanceledException ex)
-                {
-                    Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
-                    openTaskCompletionSource.TrySetException(ex);
-                }
-                catch (Exception ex)
+            return Task.Run(
+                async () =>
                 {
-                    Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
-                    openTaskCompletionSource.TrySetException(ex);
-                }
-
-                openTaskCompletionSource.TrySetResult(false);
-
-                EnableStreamSharing = false;
-                await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
-            }, CancellationToken.None);
+                    try
+                    {
+                        Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
+                        using var message = response;
+                        await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+                        await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+                        await StreamHelper.CopyToAsync(
+                            stream,
+                            fileStream,
+                            IODefaults.CopyToBufferSize,
+                            () => Resolve(openTaskCompletionSource),
+                            cancellationToken).ConfigureAwait(false);
+                    }
+                    catch (OperationCanceledException ex)
+                    {
+                        Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
+                        openTaskCompletionSource.TrySetException(ex);
+                    }
+                    catch (Exception ex)
+                    {
+                        Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
+                        openTaskCompletionSource.TrySetException(ex);
+                    }
+
+                    openTaskCompletionSource.TrySetResult(false);
+
+                    EnableStreamSharing = false;
+                    await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
+                },
+                CancellationToken.None);
         }
 
         private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)

+ 2 - 1
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -15,6 +15,7 @@ using Jellyfin.Extensions.Json.Converters;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Updates;
 using Microsoft.Extensions.DependencyInjection;
@@ -371,7 +372,7 @@ namespace Emby.Server.Implementations.Plugins
                 var url = new Uri(packageInfo.ImageUrl);
                 imagePath = Path.Join(path, url.Segments[^1]);
 
-                await using var fileStream = File.OpenWrite(imagePath);
+                await using var fileStream = AsyncFile.OpenWrite(imagePath);
 
                 try
                 {

+ 1 - 1
Emby.Server.Implementations/Serialization/MyXmlSerializer.cs

@@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.Serialization
         /// <param name="file">The file.</param>
         public void SerializeToFile(object obj, string file)
         {
-            using (var stream = new FileStream(file, FileMode.Create))
+            using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write))
             {
                 SerializeToStream(obj, stream);
             }

+ 1 - 1
Jellyfin.Api/Controllers/ImageByNameController.cs

@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             var contentType = MimeTypes.GetMimeType(path);
-            return File(System.IO.File.OpenRead(path), contentType);
+            return File(AsyncFile.OpenRead(path), contentType);
         }
 
         /// <summary>

+ 1 - 1
Jellyfin.Api/Controllers/RemoteImageController.cs

@@ -208,7 +208,7 @@ namespace Jellyfin.Api.Controllers
             var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
             Directory.CreateDirectory(fullCacheDirectory);
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-            await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+            await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
             await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
 
             var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));

+ 1 - 1
Jellyfin.Api/Controllers/SystemController.cs

@@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers
 
             // For older files, assume fully static
             var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
-            FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
+            FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
             return File(stream, "text/plain; charset=utf-8");
         }
 

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

@@ -39,7 +39,7 @@ namespace Jellyfin.Api.Helpers
                         FileAccess.Read,
                         FileShare.ReadWrite,
                         IODefaults.FileStreamBufferSize,
-                        FileOptions.SequentialScan);
+                        (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan);
                     await using (fileStream.ConfigureAwait(false))
                     {
                         using var reader = new StreamReader(fileStream);

+ 1 - 2
Jellyfin.Api/Helpers/ProgressiveFileCopier.cs

@@ -85,8 +85,7 @@ namespace Jellyfin.Api.Helpers
                 var fileOptions = FileOptions.SequentialScan;
                 var allowAsyncFileRead = false;
 
-                // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                if (AsyncFile.UseAsyncIO)
                 {
                     fileOptions |= FileOptions.Asynchronous;
                     allowAsyncFileRead = true;

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

@@ -40,7 +40,7 @@ namespace Jellyfin.Api.Helpers
             _allowAsyncFileRead = false;
 
             // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
-            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            if (AsyncFile.UseAsyncIO)
             {
                 fileOptions |= FileOptions.Asynchronous;
                 _allowAsyncFileRead = true;

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

@@ -557,7 +557,7 @@ namespace Jellyfin.Api.Helpers
                 $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
 
             // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
-            Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+            Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
 
             var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
             await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);

+ 3 - 2
Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs

@@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Users;
 
 namespace Jellyfin.Server.Implementations.Users
@@ -53,7 +54,7 @@ namespace Jellyfin.Server.Implementations.Users
             foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
             {
                 SerializablePasswordReset spr;
-                await using (var str = File.OpenRead(resetFile))
+                await using (var str = AsyncFile.OpenRead(resetFile))
                 {
                     spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false)
                         ?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid.");
@@ -110,7 +111,7 @@ namespace Jellyfin.Server.Implementations.Users
                 UserName = user.Username
             };
 
-            await using (FileStream fileStream = File.OpenWrite(filePath))
+            await using (FileStream fileStream = AsyncFile.OpenWrite(filePath))
             {
                 await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
                 await fileStream.FlushAsync().ConfigureAwait(false);

+ 2 - 1
Jellyfin.Server/Program.cs

@@ -15,6 +15,7 @@ using Jellyfin.Server.Implementations;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Configuration;
@@ -546,7 +547,7 @@ namespace Jellyfin.Server
                 ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
 
             // Copy the resource contents to the expected file path for the config file
-            await using Stream dst = File.Open(configPath, FileMode.CreateNew);
+            await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
             await resource.CopyToAsync(dst).ConfigureAwait(false);
         }
 

+ 1 - 1
MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs

@@ -89,7 +89,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
             CancellationToken cancellationToken)
         {
             var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false);
-            return File.OpenRead(attachmentPath);
+            return AsyncFile.OpenRead(attachmentPath);
         }
 
         private async Task<string> GetReadableFile(

+ 3 - 3
MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs

@@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
 
         public bool IsDir => _impl.IsDirectory;
 
-        public System.IO.Stream OpenRead()
+        public Stream OpenRead()
         {
             return new FileStream(
                 FullName,
@@ -33,9 +33,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo
                 FileShare.Read);
         }
 
-        public System.IO.StreamReader OpenText()
+        public StreamReader OpenText()
         {
-            return new System.IO.StreamReader(OpenRead());
+            return new StreamReader(OpenRead());
         }
     }
 }

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

@@ -1552,7 +1552,7 @@ namespace MediaBrowser.MediaEncoding.Probing
         {
             var packetBuffer = new byte[197];
 
-            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1))
             {
                 fs.Read(packetBuffer);
             }

+ 4 - 4
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -192,7 +192,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 }
             }
 
-            return File.OpenRead(fileInfo.Path);
+            return AsyncFile.OpenRead(fileInfo.Path);
         }
 
         private async Task<SubtitleInfo> GetReadableFile(
@@ -671,7 +671,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             string text;
             Encoding encoding;
 
-            using (var fileStream = File.OpenRead(file))
+            using (var fileStream = AsyncFile.OpenRead(file))
             using (var reader = new StreamReader(fileStream, true))
             {
                 encoding = reader.CurrentEncoding;
@@ -684,7 +684,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             if (!string.Equals(text, newText, StringComparison.Ordinal))
             {
                 // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-                using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
+                using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
                 using (var writer = new StreamWriter(fileStream, encoding))
                 {
                     await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
@@ -750,7 +750,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 }
 
                 case MediaProtocol.File:
-                    return File.OpenRead(path);
+                    return AsyncFile.OpenRead(path);
                 default:
                     throw new ArgumentOutOfRangeException(nameof(protocol));
             }

+ 34 - 0
MediaBrowser.Model/IO/AsyncFile.cs

@@ -0,0 +1,34 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Model.IO
+{
+    /// <summary>
+    /// Helper class to create async <see cref="FileStream" />s.
+    /// </summary>
+    public static class AsyncFile
+    {
+        /// <summary>
+        /// Gets a value indicating whether we should use async IO on this platform.
+        /// <see href="https://github.com/dotnet/runtime/issues/16354" />.
+        /// </summary>
+        /// <returns>Returns <c>false</c> on Windows; otherwise <c>true</c>.</returns>
+        public static bool UseAsyncIO => !OperatingSystem.IsWindows();
+
+        /// <summary>
+        /// Opens an existing file for reading.
+        /// </summary>
+        /// <param name="path">The file to be opened for reading.</param>
+        /// <returns>A read-only <see cref="FileStream" /> on the specified path.</returns>
+        public static FileStream OpenRead(string path)
+            => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, UseAsyncIO);
+
+        /// <summary>
+        /// Opens an existing file for writing.
+        /// </summary>
+        /// <param name="path">The file to be opened for writing.</param>
+        /// <returns>An unshared <see cref="FileStream" /> object on the specified path with Write access.</returns>
+        public static FileStream OpenWrite(string path)
+            => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, UseAsyncIO);
+    }
+}

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

@@ -264,7 +264,7 @@ namespace MediaBrowser.Providers.Manager
                 _fileSystem.SetAttributes(path, false, false);
 
                 // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-                await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
+                await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
                 {
                     await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
                 }

+ 1 - 1
MediaBrowser.Providers/Manager/ItemImageProvider.cs

@@ -164,7 +164,7 @@ namespace MediaBrowser.Providers.Manager
                                 {
                                     var mimeType = MimeTypes.GetMimeType(response.Path);
 
-                                    var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+                                    var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
 
                                     await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
                                 }

+ 1 - 1
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -209,7 +209,7 @@ namespace MediaBrowser.Providers.Manager
                 throw new ArgumentNullException(nameof(source));
             }
 
-            var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, true);
+            var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
 
             return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
         }

+ 2 - 1
MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs

@@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -57,7 +58,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
                 var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id);
 
-                await using FileStream jsonStream = File.OpenRead(path);
+                await using FileStream jsonStream = AsyncFile.OpenRead(path);
                 var obj = await JsonSerializer.DeserializeAsync<AudioDbAlbumProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
                 if (obj != null && obj.album != null && obj.album.Count > 0)

+ 2 - 2
MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs

@@ -66,7 +66,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
                 var path = GetAlbumInfoPath(_config.ApplicationPaths, id);
 
-                await using FileStream jsonStream = File.OpenRead(path);
+                await using FileStream jsonStream = AsyncFile.OpenRead(path);
                 var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
                 if (obj != null && obj.album != null && obj.album.Count > 0)
@@ -173,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-            await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+            await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
             await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
         }
 

+ 2 - 1
MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs

@@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -59,7 +60,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
                 var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id);
 
-                await using FileStream jsonStream = File.OpenRead(path);
+                await using FileStream jsonStream = AsyncFile.OpenRead(path);
                 var obj = await JsonSerializer.DeserializeAsync<AudioDbArtistProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
                 if (obj != null && obj.artists != null && obj.artists.Count > 0)

+ 2 - 2
MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs

@@ -65,7 +65,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
 
                 var path = GetArtistInfoPath(_config.ApplicationPaths, id);
 
-                await using FileStream jsonStream = File.OpenRead(path);
+                await using FileStream jsonStream = AsyncFile.OpenRead(path);
                 var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
                 if (obj != null && obj.artists != null && obj.artists.Count > 0)
@@ -155,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
             Directory.CreateDirectory(Path.GetDirectoryName(path));
 
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-            await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+            await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
             await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
         }
 

+ 4 - 18
MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs

@@ -236,31 +236,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb
         internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
         {
             var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
-            await using var stream = File.OpenRead(path);
+            await using var stream = AsyncFile.OpenRead(path);
             return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
         }
 
         internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken)
         {
             var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
-            await using var stream = File.OpenRead(path);
+            await using var stream = AsyncFile.OpenRead(path);
             return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
         }
 
-        internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
-        {
-            if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string id))
-            {
-                // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet.
-                if (!string.IsNullOrWhiteSpace(id))
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
         /// <summary>Gets OMDB URL.</summary>
         /// <param name="query">Appends query string to URL.</param>
         /// <returns>OMDB URL with optional query string.</returns>
@@ -309,7 +295,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
                     imdbParam));
 
             var rootObject = await GetDeserializedOmdbResponse<RootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
-            await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+            await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
             await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
             return path;
@@ -349,7 +335,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
                     seasonId));
 
             var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
-            await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+            await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
             await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
             return path;

+ 1 - 1
MediaBrowser.Providers/Studios/StudiosImageProvider.cs

@@ -146,7 +146,7 @@ namespace MediaBrowser.Providers.Studios
 
                 Directory.CreateDirectory(Path.GetDirectoryName(file));
                 await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
-                await using var fileStream = new FileStream(file, FileMode.Create);
+                await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
                 await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
             }
 

+ 1 - 1
MediaBrowser.Providers/Subtitles/SubtitleManager.cs

@@ -245,7 +245,7 @@ namespace MediaBrowser.Providers.Subtitles
                     Directory.CreateDirectory(Path.GetDirectoryName(savePath));
 
                     // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-                    using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true);
+                    using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, AsyncFile.UseAsyncIO);
                     await stream.CopyToAsync(fs).ConfigureAwait(false);
 
                     return;

+ 2 - 1
tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs

@@ -3,6 +3,7 @@ using System.Text.Json;
 using System.Threading.Tasks;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.MediaEncoding.Probing;
+using MediaBrowser.Model.IO;
 using Xunit;
 
 namespace Jellyfin.MediaEncoding.Tests
@@ -14,7 +15,7 @@ namespace Jellyfin.MediaEncoding.Tests
         public async Task Test(string fileName)
         {
             var path = Path.Join("Test Data", fileName);
-            await using (var stream = File.OpenRead(path))
+            await using (var stream = AsyncFile.OpenRead(path))
             {
                 var res = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
                 Assert.NotNull(res);