Refactor metadata handling in PLib video library manager to improve video matching accuracy. Introduce new properties for tracking when metadata was applied and from which source. Update LibraryService and related components to utilize these properties, ensuring videos without synopses are correctly identified as matched. Enhance UI to display the library's own thumbnail alongside candidate matches, improving user decision-making. Revise README.md to reflect these changes and clarify metadata application processes.

This commit is contained in:
Leonid Pershin
2026-08-10 11:29:58 +03:00
parent 4539a97d16
commit 70611c2051
20 changed files with 1387 additions and 513 deletions
@@ -1,58 +1,62 @@
using PLib.Application.Abstractions;
using PLib.Application.Library;
using PLib.Domain.Videos;
namespace PLib.Tests.Library;
/// <summary>
/// A hand-written double rather than a mock: the scan logic is all about what ends up in the
/// repository, so the tests read better when they can just look at the resulting list.
/// </summary>
internal sealed class InMemoryVideoRepository : IVideoRepository
{
private readonly Dictionary<string, VideoItem> _items = new(LibraryPathComparer.Instance);
public int SaveCount { get; private set; }
public IReadOnlyCollection<VideoItem> Items => _items.Values;
public void Seed(params VideoItem[] items)
{
foreach (var item in items)
{
_items[item.FullPath] = item;
}
}
public Task<IReadOnlyList<VideoItem>> GetAllAsync(CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<VideoItem>>([.. _items.Values]);
// Labels are held on the entity itself here, so this is the same list.
public Task<IReadOnlyList<VideoItem>> GetAllWithLabelsAsync(CancellationToken cancellationToken = default) =>
GetAllAsync(cancellationToken);
public Task<VideoItem?> FindByPathAsync(string fullPath, CancellationToken cancellationToken = default) =>
Task.FromResult(_items.GetValueOrDefault(fullPath));
// Labels are held on the entity itself here, so there is nothing extra to load.
public Task<VideoItem?> FindWithLabelsAsync(Guid id, CancellationToken cancellationToken = default) =>
Task.FromResult(_items.Values.FirstOrDefault(item => item.Id == id));
public Task AddAsync(VideoItem item, CancellationToken cancellationToken = default)
{
_items[item.FullPath] = item;
return Task.CompletedTask;
}
public Task RemoveAsync(VideoItem item, CancellationToken cancellationToken = default)
{
_items.Remove(item.FullPath);
return Task.CompletedTask;
}
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
SaveCount++;
return Task.CompletedTask;
}
}
using PLib.Application.Abstractions;
using PLib.Application.Library;
using PLib.Domain.Videos;
namespace PLib.Tests.Library;
/// <summary>
/// A hand-written double rather than a mock: the scan logic is all about what ends up in the
/// repository, so the tests read better when they can just look at the resulting list.
/// </summary>
internal sealed class InMemoryVideoRepository : IVideoRepository
{
private readonly Dictionary<string, VideoItem> _items = new(LibraryPathComparer.Instance);
public int SaveCount { get; private set; }
public IReadOnlyCollection<VideoItem> Items => _items.Values;
public void Seed(params VideoItem[] items)
{
foreach (var item in items)
{
_items[item.FullPath] = item;
}
}
public Task<IReadOnlyList<VideoItem>> GetAllAsync(CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<VideoItem>>([.. _items.Values]);
// Labels are held on the entity itself here, so this is the same list.
public Task<IReadOnlyList<VideoItem>> GetAllWithLabelsAsync(CancellationToken cancellationToken = default) =>
GetAllAsync(cancellationToken);
// Applied sources live on the entity here, so this is the same list again.
public Task<IReadOnlyList<VideoItem>> GetAllWithMetadataSourcesAsync(CancellationToken cancellationToken = default) =>
GetAllAsync(cancellationToken);
public Task<VideoItem?> FindByPathAsync(string fullPath, CancellationToken cancellationToken = default) =>
Task.FromResult(_items.GetValueOrDefault(fullPath));
// Labels are held on the entity itself here, so there is nothing extra to load.
public Task<VideoItem?> FindWithLabelsAsync(Guid id, CancellationToken cancellationToken = default) =>
Task.FromResult(_items.Values.FirstOrDefault(item => item.Id == id));
public Task AddAsync(VideoItem item, CancellationToken cancellationToken = default)
{
_items[item.FullPath] = item;
return Task.CompletedTask;
}
public Task RemoveAsync(VideoItem item, CancellationToken cancellationToken = default)
{
_items.Remove(item.FullPath);
return Task.CompletedTask;
}
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
SaveCount++;
return Task.CompletedTask;
}
}