properly restore all local library items from trash during scans (#1660)

This commit is contained in:
Jason Dove
2024-03-29 20:03:19 -05:00
committed by GitHub
parent 14a707a4e2
commit 7702999b9a
9 changed files with 106 additions and 87 deletions
+1
View File
@@ -55,6 +55,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix bug where replacing files in Plex would be missed by subsequent ETV library scans
- This fix will require a one-time re-scan of each Plex library in full
- After the initial full scan, incremental scans will behave as normal
- Fix edge case where some local episodes, music videos, other videos, songs, images would not automatically be restored from trash
### Changed
- Log search index updates under scanner category at debug level, to indicate a potential cause for the UI being out of date
@@ -18,5 +18,4 @@ public interface ILibraryRepository
Task<LibraryFolder> GetOrAddFolder(LibraryPath libraryPath, Option<int> maybeParentFolder, string folder);
Task UpdateLibraryFolderId(MediaFile mediaFile, int libraryFolderId);
Task UpdatePath(LibraryPath libraryPath, string normalizedLibraryPath);
Task<System.Collections.Generic.HashSet<string>> FindAllMissingFiles(LibraryPath libraryPath);
}
@@ -1,6 +1,5 @@
using Dapper;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Extensions;
@@ -212,41 +211,6 @@ public class LibraryRepository : ILibraryRepository
new { Path = normalizedLibraryPath, libraryPath.Id });
}
public async Task<System.Collections.Generic.HashSet<string>> FindAllMissingFiles(LibraryPath libraryPath)
{
var result = new System.Collections.Generic.HashSet<string>();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
IAsyncEnumerable<MediaItem> items = dbContext.MediaItems
.AsNoTracking()
.Filter(mi => mi.LibraryPathId == libraryPath.Id)
.Filter(mi => mi.State == MediaItemState.FileNotFound)
.Include(mi => (mi as Movie).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Song).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Image).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.AsAsyncEnumerable();
await foreach (MediaItem item in items)
{
MediaVersion version = item.GetHeadVersion();
foreach (MediaFile file in version.MediaFiles)
{
result.Add(file.Path);
}
}
return result;
}
private static LibraryFolder CreateNewFolder(LibraryPath libraryPath, Option<int> maybeParentFolder, string folder)
{
int? parentId = null;
@@ -91,7 +91,7 @@ public class MediaItemRepository : IMediaItemRepository
return await dbContext.Connection.QueryAsync<string>(
@"SELECT MF.Path
FROM MediaItem M
INNER JOIN MediaVersion MV on M.Id = COALESCE(MovieId, MusicVideoId, OtherVideoId, SongId, EpisodeId)
INNER JOIN MediaVersion MV on M.Id = COALESCE(MovieId, MusicVideoId, OtherVideoId, SongId, EpisodeId, ImageId)
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId
WHERE M.State IN (1,2) AND M.LibraryPathId = @LibraryPathId",
new { LibraryPathId = libraryPath.Id })
@@ -1,4 +1,5 @@
using Bugsnag;
using System.Collections.Immutable;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
@@ -20,6 +21,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
private readonly IClient _client;
private readonly IImageRepository _imageRepository;
private readonly ILibraryRepository _libraryRepository;
private readonly IMediaItemRepository _mediaItemRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILogger<ImageFolderScanner> _logger;
@@ -54,6 +56,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
_mediator = mediator;
_imageRepository = imageRepository;
_libraryRepository = libraryRepository;
_mediaItemRepository = mediaItemRepository;
_client = client;
_logger = logger;
}
@@ -83,6 +86,8 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
ImmutableHashSet<string> allTrashedItems = await _mediaItemRepository.GetAllTrashedItems(libraryPath);
if (ShouldIncludeFolder(libraryPath.Path) && allFolders.Add(libraryPath.Path))
{
folderQueue.Enqueue(libraryPath.Path);
@@ -139,10 +144,23 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
maybeParentFolder,
imageFolder);
// skip folder if etag matches
if (allFiles.Count == 0 || knownFolder.Etag == etag)
if (knownFolder.Etag == etag)
{
continue;
if (allFiles.Any(allTrashedItems.Contains))
{
_logger.LogDebug("Previously trashed items are now present in folder {Folder}", imageFolder);
}
else
{
// etag matches and no trashed items are now present, continue to next folder
continue;
}
}
else
{
_logger.LogDebug(
"UPDATE: Etag has changed for folder {Folder}",
imageFolder);
}
// walk up to get duration, if needed
@@ -1,4 +1,5 @@
using Bugsnag;
using System.Collections.Immutable;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
@@ -20,6 +21,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
private readonly IArtistRepository _artistRepository;
private readonly IClient _client;
private readonly ILibraryRepository _libraryRepository;
private readonly IMediaItemRepository _mediaItemRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
@@ -59,6 +61,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
_artistRepository = artistRepository;
_musicVideoRepository = musicVideoRepository;
_libraryRepository = libraryRepository;
_mediaItemRepository = mediaItemRepository;
_mediator = mediator;
_client = client;
_logger = logger;
@@ -84,6 +87,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
ImmutableHashSet<string> allTrashedItems = await _mediaItemRepository.GetAllTrashedItems(libraryPath);
var allArtistFolders = _localFileSystem.ListSubdirectories(libraryPath.Path)
.Filter(ShouldIncludeFolder)
.OrderBy(identity)
@@ -151,6 +156,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
ffprobePath,
result.Item,
artistFolder,
allTrashedItems,
cancellationToken);
foreach (ScanCanceled error in scanResult.LeftToSeq().OfType<ScanCanceled>())
@@ -312,6 +318,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
string ffprobePath,
Artist artist,
string artistFolder,
ImmutableHashSet<string> allTrashedItems,
CancellationToken cancellationToken)
{
var folderQueue = new Queue<string>();
@@ -345,10 +352,23 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
maybeParentFolder,
musicVideoFolder);
// skip folder if etag matches
if (knownFolder.Etag == etag)
{
continue;
if (allFiles.Any(allTrashedItems.Contains))
{
_logger.LogDebug("Previously trashed items are now present in folder {Folder}", musicVideoFolder);
}
else
{
// etag matches and no trashed items are now present, continue to next folder
continue;
}
}
else
{
_logger.LogDebug(
"UPDATE: Etag has changed for folder {Folder}",
musicVideoFolder);
}
var hasErrors = false;
@@ -1,4 +1,5 @@
using Bugsnag;
using System.Collections.Immutable;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
@@ -19,6 +20,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
{
private readonly IClient _client;
private readonly ILibraryRepository _libraryRepository;
private readonly IMediaItemRepository _mediaItemRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
@@ -57,6 +59,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
_mediator = mediator;
_otherVideoRepository = otherVideoRepository;
_libraryRepository = libraryRepository;
_mediaItemRepository = mediaItemRepository;
_client = client;
_logger = logger;
}
@@ -78,16 +81,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
var allFolders = new System.Collections.Generic.HashSet<string>();
var folderQueue = new Queue<string>();
System.Collections.Generic.HashSet<string> allMissingFiles =
await _libraryRepository.FindAllMissingFiles(libraryPath);
if (allMissingFiles.Count > 0)
{
_logger.LogDebug(
"Library path {Path} has {Count} missing files",
libraryPath.Path,
allMissingFiles.Count);
}
ImmutableHashSet<string> allTrashedItems = await _mediaItemRepository.GetAllTrashedItems(libraryPath);
string normalizedLibraryPath = libraryPath.Path.TrimEnd(
Path.DirectorySeparatorChar,
@@ -160,35 +154,25 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
maybeParentFolder,
otherVideoFolder);
bool hasMissingFiles = allFiles.Any(allMissingFiles.Contains);
bool isSameEtag = !hasMissingFiles && knownFolder.Etag == etag;
_logger.LogDebug(
"Scanning other video folder {Folder}; file count: {Count}, etag: {LastEtag} => {Etag}; has missing files: {HasMissingFiles}",
otherVideoFolder,
allFiles.Count,
etag,
knownFolder.Etag,
hasMissingFiles);
// skip empty folder
if (allFiles.Count == 0)
if (knownFolder.Etag == etag)
{
_logger.LogDebug("Skipping empty other videos folder");
continue;
if (allFiles.Any(allTrashedItems.Contains))
{
_logger.LogDebug("Previously trashed items are now present in folder {Folder}", otherVideoFolder);
}
else
{
// etag matches and no trashed items are now present, continue to next folder
continue;
}
}
// skip folder if etag matches
if (isSameEtag)
else
{
_logger.LogDebug("Skipping unchanged other videos folder, that contains no missing items");
continue;
_logger.LogDebug(
"UPDATE: Etag has changed for folder {Folder}",
otherVideoFolder);
}
_logger.LogDebug(
"UPDATE: Etag has changed for folder {Folder}",
otherVideoFolder);
var hasErrors = false;
foreach (string file in allFiles.OrderBy(identity))
@@ -1,4 +1,5 @@
using Bugsnag;
using System.Collections.Immutable;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
@@ -19,6 +20,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
{
private readonly IClient _client;
private readonly ILibraryRepository _libraryRepository;
private readonly IMediaItemRepository _mediaItemRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILogger<SongFolderScanner> _logger;
@@ -54,6 +56,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
_mediator = mediator;
_songRepository = songRepository;
_libraryRepository = libraryRepository;
_mediaItemRepository = mediaItemRepository;
_client = client;
_logger = logger;
}
@@ -82,6 +85,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
ImmutableHashSet<string> allTrashedItems = await _mediaItemRepository.GetAllTrashedItems(libraryPath);
if (ShouldIncludeFolder(libraryPath.Path))
{
folderQueue.Enqueue(libraryPath.Path);
@@ -135,11 +140,24 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
libraryPath,
maybeParentFolder,
songFolder);
// skip folder if etag matches
if (allFiles.Count == 0 || knownFolder.Etag == etag)
if (knownFolder.Etag == etag)
{
continue;
if (allFiles.Any(allTrashedItems.Contains))
{
_logger.LogDebug("Previously trashed items are now present in folder {Folder}", songFolder);
}
else
{
// etag matches and no trashed items are now present, continue to next folder
continue;
}
}
else
{
_logger.LogDebug(
"UPDATE: Etag has changed for folder {Folder}",
songFolder);
}
_logger.LogDebug(
@@ -1,4 +1,5 @@
using Bugsnag;
using System.Collections.Immutable;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
@@ -20,6 +21,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
private readonly IClient _client;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILibraryRepository _libraryRepository;
private readonly IMediaItemRepository _mediaItemRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
@@ -60,6 +62,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
_localSubtitlesProvider = localSubtitlesProvider;
_metadataRepository = metadataRepository;
_libraryRepository = libraryRepository;
_mediaItemRepository = mediaItemRepository;
_mediator = mediator;
_client = client;
_fallbackMetadataProvider = fallbackMetadataProvider;
@@ -85,6 +88,8 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
ImmutableHashSet<string> allTrashedItems = await _mediaItemRepository.GetAllTrashedItems(libraryPath);
var allShowFolders = _localFileSystem.ListSubdirectories(libraryPath.Path)
.Filter(ShouldIncludeFolder)
@@ -153,6 +158,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
ffprobePath,
result.Item,
showFolder,
allTrashedItems,
cancellationToken);
foreach (ScanCanceled error in scanResult.LeftToSeq().OfType<ScanCanceled>())
@@ -227,6 +233,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
string ffprobePath,
Show show,
string showFolder,
ImmutableHashSet<string> allTrashedItems,
CancellationToken cancellationToken)
{
foreach (string seasonFolder in _localFileSystem.ListSubdirectories(showFolder).Filter(ShouldIncludeFolder)
@@ -248,7 +255,15 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
// skip folder if etag matches
if (knownFolder.Etag == etag)
{
continue;
if (allTrashedItems.Any(f => f.StartsWith(seasonFolder, StringComparison.OrdinalIgnoreCase)))
{
_logger.LogDebug("Previously trashed items are now present in folder {Folder}", seasonFolder);
}
else
{
// etag matches and no trashed items are now present, continue to next folder
continue;
}
}
Option<int> maybeSeasonNumber = _fallbackMetadataProvider.GetSeasonNumberForFolder(seasonFolder);