aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc Brooks <IDisposable@gmail.com>2026-05-22 16:53:58 -0500
committerMarc Brooks <IDisposable@gmail.com>2026-05-26 18:12:31 -0500
commit961c6d3d547a1e3c6352129b45fffd1158571d15 (patch)
tree8763a04091f920748749615bda1992939342f846
parent4af66c4e1ad0f8c0105dc3a48c2bfaf29cd11750 (diff)
Compare old file byte-by-byte to new stream
Don't overwrite if identical.
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs76
1 files changed, 73 insertions, 3 deletions
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index ed32e6c76a..64daac68e3 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -198,15 +198,23 @@ namespace MediaBrowser.XbmcMetadata.Savers
cancellationToken.ThrowIfCancellationRequested();
- await SaveToFileAsync(memoryStream, path).ConfigureAwait(false);
+ await SaveToFileAsync(memoryStream, path, cancellationToken).ConfigureAwait(false);
}
}
- private async Task SaveToFileAsync(Stream stream, string path)
+ private async Task SaveToFileAsync(Stream stream, string path, CancellationToken cancellationToken)
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
+ // Compare byte-for-byte before proceeding.
+ if (File.Exists(path) && await IsFileIdenticalAsync(stream, path, cancellationToken).ConfigureAwait(false))
+ {
+ return; // Don't save since .nfo is unchanged.
+ }
+
+ stream.Position = 0;
+
// On Windows, saving the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
@@ -222,7 +230,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
var filestream = new FileStream(path, fileStreamOptions);
await using (filestream.ConfigureAwait(false))
{
- await stream.CopyToAsync(filestream).ConfigureAwait(false);
+ await stream.CopyToAsync(filestream, cancellationToken).ConfigureAwait(false);
}
if (ConfigurationManager.Configuration.SaveMetadataHidden)
@@ -231,6 +239,68 @@ 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