Refactor PLib video library manager to use ReactiveUI, replacing CommunityToolkit.Mvvm. Update dependencies, enhance thumbnail caching logic, and improve UI responsiveness with reactive commands. Remove obsolete PLib.slnx file and update README.md to reflect changes.

This commit is contained in:
Leonid Pershin
2026-08-08 11:37:36 +03:00
parent bf4a3bb922
commit 625eae7ead
18 changed files with 693 additions and 359 deletions
@@ -18,6 +18,15 @@ public sealed class FfmpegThumbnailGenerator(
/// <summary>Fallback capture position for files whose duration we could not read.</summary>
private static readonly TimeSpan BlindCapturePosition = TimeSpan.FromSeconds(5);
/// <summary>
/// How long an unfinished render is left alone before it counts as abandoned. A second
/// instance of the application could be mid-render right now, and deleting its staging
/// file would silently cost it the frame.
/// </summary>
private static readonly TimeSpan AbandonedRenderAge = TimeSpan.FromHours(1);
private const string StagingExtension = ".tmp";
private readonly LibraryOptions _options = options.Value;
public async Task<string?> GetOrCreateAsync(
@@ -41,7 +50,7 @@ public sealed class FfmpegThumbnailGenerator(
// Render to a private temp file first so a crash or cancellation can never leave a
// truncated JPEG behind that later runs would happily treat as a valid cache hit.
var staging = Path.Combine(paths.ThumbnailDirectory, $"{Guid.CreateVersion7()}.tmp");
var staging = Path.Combine(paths.ThumbnailDirectory, $"{Guid.CreateVersion7()}{StagingExtension}");
try
{
@@ -73,6 +82,58 @@ public sealed class FfmpegThumbnailGenerator(
}
}
public bool IsAvailable(string? thumbnailPath) =>
!string.IsNullOrEmpty(thumbnailPath) && File.Exists(thumbnailPath);
public Task<int> PurgeUnusedAsync(
IReadOnlyCollection<string> inUsePaths,
CancellationToken cancellationToken = default) =>
Task.Run(() => Purge(inUsePaths, cancellationToken), cancellationToken);
private int Purge(IReadOnlyCollection<string> inUsePaths, CancellationToken cancellationToken)
{
if (!Directory.Exists(paths.ThumbnailDirectory))
{
return 0;
}
var inUse = new HashSet<string>(inUsePaths, LibraryPathComparer.Instance);
var removed = 0;
foreach (var file in Directory.EnumerateFiles(paths.ThumbnailDirectory))
{
cancellationToken.ThrowIfCancellationRequested();
if (!ShouldRemove(file, inUse))
{
continue;
}
try
{
File.Delete(file);
removed++;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Somebody else is holding the file; the next scan will try again.
logger.LogDebug(ex, "Could not remove the cached frame {Path}", file);
}
}
return removed;
}
private bool ShouldRemove(string file, HashSet<string> inUse)
{
if (file.EndsWith(StagingExtension, StringComparison.OrdinalIgnoreCase))
{
return File.GetLastWriteTimeUtc(file) < DateTime.UtcNow - AbandonedRenderAge;
}
return !inUse.Contains(file);
}
private async Task<bool> RenderAsync(
string videoPath,
string outputPath,