diff --git a/CHANGELOG.md b/CHANGELOG.md index e65772e11..b1a14879c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `` from NFO metadata is now searchable with `country:` instead of `tag:`; deep scan a local library to convert existing items - Fix multiple channel startup failure causes - Fix seeking more than 24 hours into content +- Fix local movie and television folder scanner to only set etag when all files process successfully + - Previously, errors would be ignored so problematic files (e.g. malformed nfo files) would need to be touched for ETV to attempt reading them again ## [26.8.1] - 2026-08-29 ### Security diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj b/ErsatzTV.Application/ErsatzTV.Application.csproj index dce9fc9ae..1310b5bff 100644 --- a/ErsatzTV.Application/ErsatzTV.Application.csproj +++ b/ErsatzTV.Application/ErsatzTV.Application.csproj @@ -11,7 +11,7 @@ - + diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index 83ff7de51..e8e36c180 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -7,17 +7,17 @@ - + - + - + diff --git a/ErsatzTV.Core/ErsatzTV.Core.csproj b/ErsatzTV.Core/ErsatzTV.Core.csproj index 61ae5cd70..21dca3586 100644 --- a/ErsatzTV.Core/ErsatzTV.Core.csproj +++ b/ErsatzTV.Core/ErsatzTV.Core.csproj @@ -25,8 +25,8 @@ - - + + diff --git a/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj b/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj index 393114cec..6e0822761 100644 --- a/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj +++ b/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj @@ -10,10 +10,10 @@ - + - + all diff --git a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj index cfa2c030c..40790c79f 100644 --- a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj +++ b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj @@ -11,10 +11,10 @@ - + - + diff --git a/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj b/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj index b19a54067..8d46a7696 100644 --- a/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj +++ b/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj @@ -9,10 +9,10 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index e85101a73..77273873e 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -13,12 +13,12 @@ - + - + - + @@ -30,13 +30,13 @@ - - - + + + - + - + diff --git a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj index c0f12c985..fcacd05df 100644 --- a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj +++ b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj @@ -10,10 +10,10 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/ErsatzTV.Scanner/Core/Errors/ErrorProcessingFolder.cs b/ErsatzTV.Scanner/Core/Errors/ErrorProcessingFolder.cs new file mode 100644 index 000000000..4f9d69edf --- /dev/null +++ b/ErsatzTV.Scanner/Core/Errors/ErrorProcessingFolder.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Scanner.Core.Errors; + +public class ErrorProcessingFolder() : BaseError("Error processing folder"); diff --git a/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs index ee6b7ec78..d4f530cba 100644 --- a/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs @@ -211,6 +211,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) { _logger.LogWarning("Failed to reindex media items from scanner process"); + hasErrors = true; } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs index 4ca40390a..b38e61efe 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs @@ -174,6 +174,8 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner movieFolder); } + var hasErrors = false; + foreach (string file in allFiles.OrderBy(identity)) { // TODO: figure out how to rebuild playlists @@ -191,6 +193,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner foreach (BaseError error in maybeMovie.LeftToSeq()) { _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value); + hasErrors = true; } foreach (MediaItemScanResult result in maybeMovie.RightToSeq()) @@ -200,12 +203,17 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) { _logger.LogWarning("Failed to reindex media items from scanner process"); + hasErrors = true; } } - - await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag); } } + + // only do this once per folder and only if all files processed successfully + if (!hasErrors) + { + await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag); + } } foreach (string path in await _movieRepository.FindMoviePaths(libraryPath)) diff --git a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs index 6b26ba95a..57ea2df6c 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs @@ -387,6 +387,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) { _logger.LogWarning("Failed to reindex media items from scanner process"); + hasErrors = true; } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs index d17c4bb42..eae00ee8b 100644 --- a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs @@ -207,6 +207,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan if (!await _scannerProxy.ReindexMediaItems([result.Item.Id], cancellationToken)) { _logger.LogWarning("Failed to reindex media items from scanner process"); + hasErrors = true; } } } diff --git a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs index 4d6b18764..7fea64a38 100644 --- a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs @@ -9,6 +9,7 @@ using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Errors; using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.Metadata; @@ -293,14 +294,20 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan return error; } - await _libraryRepository.SetEtag(libraryPath, knownFolder, seasonFolder, etag); - - season.Show = show; - - if (!await _scannerProxy.ReindexMediaItems([season.Id], cancellationToken)) + // only set etag if no errors were encountered with the folder, and if reindex succeeds + if (scanResult.IsRight) { - _logger.LogWarning("Failed to reindex media items from scanner process"); + if (await _scannerProxy.ReindexMediaItems([season.Id], cancellationToken)) + { + await _libraryRepository.SetEtag(libraryPath, knownFolder, seasonFolder, etag); + } + else + { + _logger.LogWarning("Failed to reindex media items from scanner process"); + } } + + season.Show = show; } } } @@ -326,8 +333,15 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan .OrderBy(identity) .ToList(); + var hasErrors = false; + foreach (string file in allSeasonFiles) { + if (cancellationToken.IsCancellationRequested) + { + return new ScanCanceled(); + } + // TODO: figure out how to rebuild playlists Either maybeEpisode = await _televisionRepository .GetOrAddEpisode(season, libraryPath, seasonFolder, file, cancellationToken) @@ -344,6 +358,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan foreach (BaseError error in maybeEpisode.LeftToSeq()) { _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value); + hasErrors = true; } foreach (Episode episode in maybeEpisode.RightToSeq()) @@ -351,12 +366,18 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan if (!await _scannerProxy.ReindexMediaItems([episode.Id], cancellationToken)) { _logger.LogWarning("Failed to reindex media items from scanner process"); + hasErrors = true; } } } // TODO: remove missing episodes? + if (hasErrors) + { + return new ErrorProcessingFolder(); + } + return Unit.Default; } diff --git a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj index 42543c300..12eb593ba 100644 --- a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj +++ b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj @@ -21,7 +21,7 @@ - + diff --git a/ErsatzTV.Tests/ErsatzTV.Tests.csproj b/ErsatzTV.Tests/ErsatzTV.Tests.csproj index c7c4506c2..4e7bf4987 100644 --- a/ErsatzTV.Tests/ErsatzTV.Tests.csproj +++ b/ErsatzTV.Tests/ErsatzTV.Tests.csproj @@ -12,13 +12,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/ErsatzTV.sln b/ErsatzTV.sln index d1d9dd8b4..7c02d3942 100644 --- a/ErsatzTV.sln +++ b/ErsatzTV.sln @@ -30,6 +30,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.Core.Nullable", "E EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.Tests", "ErsatzTV.Tests\ErsatzTV.Tests.csproj", "{56F56E76-CEF4-4639-B7BB-03FD201BB019}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Markdown", "Markdown", "{E4F90ABA-8D7B-4DD6-8D10-0DFC54F62611}" + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index bc8a812c2..01520b5d1 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -36,8 +36,8 @@ - - + + @@ -54,10 +54,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + +