diff options
| author | Marc Brooks <IDisposable@gmail.com> | 2026-05-22 16:53:58 -0500 |
|---|---|---|
| committer | Marc Brooks <IDisposable@gmail.com> | 2026-05-26 18:12:31 -0500 |
| commit | 961c6d3d547a1e3c6352129b45fffd1158571d15 (patch) | |
| tree | 8763a04091f920748749615bda1992939342f846 | |
| parent | 4af66c4e1ad0f8c0105dc3a48c2bfaf29cd11750 (diff) | |
Compare old file byte-by-byte to new stream
Don't overwrite if identical.
| -rw-r--r-- | MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs | 76 |
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 |
