Refactor derived data management in PLib video library manager. Introduce functionality to clear specific types of derived data, including thumbnails, animated previews, perceptual hashes, and technical metadata. Update ILibraryService and LibraryService to support new data usage reporting and reset operations. Revise SettingsViewModel and UI components to reflect these changes, enhancing user control over data management. Update README.md to document new features and usage instructions.

This commit is contained in:
Leonid Pershin
2026-08-09 08:00:26 +03:00
parent 3c77baced7
commit c41a458d0b
11 changed files with 371 additions and 63 deletions
@@ -231,6 +231,85 @@ public sealed class LibraryServiceTests
events.OfType<LibraryScanEvent.DiscoveryCompleted>().Single().FilesFound.ShouldBe(1);
}
[Fact]
public async Task Clearing_one_kind_of_derived_data_leaves_every_other_kind_alone()
{
var item = FullyIndexed();
_repository.Seed(item);
await CreateService().ResetAsync(LibraryDataKind.PerceptualHashes, Token);
item.PerceptualHash.ShouldBeNull();
// Clearing the hash is a database edit; nothing on disk has anything to do with it.
item.ThumbnailPath.ShouldNotBeNull();
item.PreviewPath.ShouldNotBeNull();
item.Duration.ShouldNotBeNull();
await _thumbnails.DidNotReceive().ClearAsync(Arg.Any<CancellationToken>());
await _previews.DidNotReceive().ClearAsync(Arg.Any<CancellationToken>());
}
[Fact]
public async Task Clearing_an_image_cache_deletes_the_files_as_well_as_the_references()
{
var item = FullyIndexed();
_repository.Seed(item);
await CreateService().ResetAsync(LibraryDataKind.AnimatedPreviews, Token);
// A path kept after the file was deleted would leave the card pointing at nothing.
item.PreviewPath.ShouldBeNull();
await _previews.Received(1).ClearAsync(Arg.Any<CancellationToken>());
await _thumbnails.DidNotReceive().ClearAsync(Arg.Any<CancellationToken>());
item.ThumbnailPath.ShouldNotBeNull();
}
[Fact]
public async Task Clearing_everything_takes_all_four_kinds()
{
var item = FullyIndexed();
_repository.Seed(item);
await CreateService().ResetAsync(LibraryDataKind.All, Token);
item.ThumbnailPath.ShouldBeNull();
item.PreviewPath.ShouldBeNull();
item.PerceptualHash.ShouldBeNull();
item.Duration.ShouldBeNull();
item.IsIndexed.ShouldBeFalse();
}
[Fact]
public async Task Usage_says_how_far_each_kind_has_got_through_the_library()
{
_repository.Seed(FullyIndexed());
_repository.Seed(new VideoItem(@"C:\videos\b.mp4", "b", 5_000, DateTimeOffset.UnixEpoch));
_previews.GetCacheSizeInBytesAsync(Arg.Any<CancellationToken>()).Returns(4_096L);
var usage = (await CreateService().GetDataUsageAsync(Token)).ToDictionary(entry => entry.Kind);
usage[LibraryDataKind.PerceptualHashes].ShouldSatisfyAllConditions(
entry => entry.Videos.ShouldBe(2),
entry => entry.Present.ShouldBe(1));
// Only the caches that are files on disk have a size to report.
usage[LibraryDataKind.AnimatedPreviews].Bytes.ShouldBe(4_096L);
usage[LibraryDataKind.PerceptualHashes].Bytes.ShouldBe(0);
}
private static CancellationToken Token => TestContext.Current.CancellationToken;
private static VideoItem FullyIndexed()
{
var item = new VideoItem(@"C:\videos\a.mp4", "a", 5_000, DateTimeOffset.UnixEpoch);
item.ApplyTechnicalInfo(new VideoTechnicalInfo(TimeSpan.FromMinutes(1), 1280, 720, "h264"));
item.AttachThumbnail(@"C:\cache\a.jpg");
item.AttachPreview(@"C:\cache\a.strip.jpg", 12);
item.ApplyPerceptualHash(1);
return item;
}
private static DiscoveredVideoFile File(string path, long sizeInBytes = 10_000) =>
new(path, sizeInBytes, DateTimeOffset.UnixEpoch);
+24
View File
@@ -0,0 +1,24 @@
using System.Runtime.CompilerServices;
using ReactiveUI.Builder;
namespace PLib.Tests;
/// <summary>
/// Brings ReactiveUI up before any test touches a view model.
/// </summary>
/// <remarks>
/// ReactiveUI 24 refuses to serve <c>WhenAnyValue</c> until it has been initialised, and the
/// application does that in <c>Program.Main</c> — which a test host never runs. Only the core
/// services are registered here: platform services are about dispatchers and bindings, and a
/// view model under test has neither.
/// </remarks>
internal static class ReactiveUiBootstrap
{
[ModuleInitializer]
internal static void Initialize() =>
// BuildApp rather than Build: only the former marks ReactiveUI as initialised, and
// that flag is exactly what WhenAnyValue checks.
RxAppBuilder.CreateReactiveUIBuilder()
.WithCoreServices()
.BuildApp();
}