aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc Brooks <IDisposable@gmail.com>2026-05-26 22:37:17 +0000
committerMarc Brooks <IDisposable@gmail.com>2026-05-26 18:12:31 -0500
commit02ca63cd13779dbff9971e10a7afd62d2634337b (patch)
tree554ca1f5cd1a5e005d7ad9577da607be786258e7
parent961c6d3d547a1e3c6352129b45fffd1158571d15 (diff)
Moved IsFileIdenticalAsync & IsStreamIdenticalAsync to StreamExtensions.
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs64
-rw-r--r--src/Jellyfin.Extensions/StreamExtensions.cs96
2 files changed, 97 insertions, 63 deletions
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 64daac68e3..78907a5e68 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -208,7 +208,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
Directory.CreateDirectory(directory);
// Compare byte-for-byte before proceeding.
- if (File.Exists(path) && await IsFileIdenticalAsync(stream, path, cancellationToken).ConfigureAwait(false))
+ if (File.Exists(path) && await stream.IsFileIdenticalAsync(path, cancellationToken).ConfigureAwait(false))
{
return; // Don't save since .nfo is unchanged.
}
@@ -239,68 +239,6 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
}
- private static async Task<bool> IsFileIdenticalAsync(Stream stream, string path, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(stream);
- ArgumentException.ThrowIfNullOrEmpty(path);
-
- if (!stream.CanSeek)
- {
- return false;
- }
-
- const int BufferSize = 81920;
- var originalPosition = stream.Position;
-
- try
- {
- stream.Position = 0;
-
- using var existingFileStream = new FileStream(
- path,
- FileMode.Open,
- FileAccess.Read,
- FileShare.Read,
- bufferSize: BufferSize,
- FileOptions.Asynchronous);
-
- if (existingFileStream.Length != stream.Length)
- {
- return false;
- }
-
- var streamBuffer = new byte[BufferSize];
- var existingBuffer = new byte[BufferSize];
-
- while (true)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var streamBytesRead = await stream.ReadAsync(streamBuffer.AsMemory(), cancellationToken).ConfigureAwait(false);
- var existingBytesRead = await existingFileStream.ReadAsync(existingBuffer.AsMemory(), cancellationToken).ConfigureAwait(false);
-
- if (streamBytesRead != existingBytesRead)
- {
- return false;
- }
-
- if (streamBytesRead == 0)
- {
- return true;
- }
-
- if (!streamBuffer.AsSpan(0, streamBytesRead).SequenceEqual(existingBuffer.AsSpan(0, existingBytesRead)))
- {
- return false;
- }
- }
- }
- finally
- {
- stream.Position = originalPosition;
- }
- }
-
private void SetHidden(string path, bool hidden)
{
try
diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs
index 0cfac384e3..fa019b0059 100644
--- a/src/Jellyfin.Extensions/StreamExtensions.cs
+++ b/src/Jellyfin.Extensions/StreamExtensions.cs
@@ -1,9 +1,12 @@
+using System;
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
+using System.Threading.Tasks;
namespace Jellyfin.Extensions
{
@@ -12,6 +15,8 @@ namespace Jellyfin.Extensions
/// </summary>
public static class StreamExtensions
{
+ private const int StreamComparisonBufferSize = 65536;
+
/// <summary>
/// Reads all lines in the <see cref="Stream" />.
/// </summary>
@@ -60,5 +65,96 @@ namespace Jellyfin.Extensions
yield return line;
}
}
+
+ /// <summary>
+ /// Determines whether a stream is identical to a file on disk.
+ /// </summary>
+ /// <param name="stream">The stream to compare.</param>
+ /// <param name="path">The file path to compare against.</param>
+ /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+ /// <returns>True if the stream and file are identical; otherwise false.</returns>
+ public static async Task<bool> IsFileIdenticalAsync(this Stream stream, string path, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ if (!stream.CanSeek)
+ {
+ return false;
+ }
+
+ var originalPosition = stream.Position;
+ try
+ {
+ stream.Position = 0;
+
+ var existingFileStream = new FileStream(
+ path,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read,
+ bufferSize: StreamComparisonBufferSize,
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
+ await using (existingFileStream.ConfigureAwait(false))
+ {
+ return await stream.IsStreamIdenticalAsync(existingFileStream, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ stream.Position = originalPosition;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether two streams are identical.
+ /// </summary>
+ /// <param name="a">The first stream to compare.</param>
+ /// <param name="b">The second stream to compare.</param>
+ /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+ /// <returns>True if the streams are identical; otherwise false.</returns>
+ public static async Task<bool> IsStreamIdenticalAsync(this Stream a, Stream b, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(a);
+ ArgumentNullException.ThrowIfNull(b);
+
+ if (b.Length != a.Length)
+ {
+ return false;
+ }
+
+ var bufferA = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
+ var bufferB = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
+ try
+ {
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var bytesReadA = await a.ReadAsync(bufferA.AsMemory(), cancellationToken).ConfigureAwait(false);
+ var bytesReadB = await b.ReadAsync(bufferB.AsMemory(), cancellationToken).ConfigureAwait(false);
+
+ if (bytesReadA != bytesReadB)
+ {
+ return false;
+ }
+
+ if (bytesReadA == 0)
+ {
+ return true;
+ }
+
+ if (!bufferA.AsSpan(0, bytesReadA).SequenceEqual(bufferB.AsSpan(0, bytesReadB)))
+ {
+ return false;
+ }
+ }
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(bufferA);
+ ArrayPool<byte>.Shared.Return(bufferB);
+ }
+ }
}
}