Update README.md with project details, features, requirements, architecture, and data management for PLib video library manager.

This commit is contained in:
Leonid Pershin
2026-08-08 07:08:43 +03:00
parent e767e4a48c
commit ac05ea6b7f
59 changed files with 3010 additions and 0 deletions
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace PLib.Infrastructure.Persistence;
/// <summary>
/// Brings the local database up to date before the first window is shown.
/// </summary>
/// <remarks>
/// While the schema is still moving we create it from the model. Once the shape settles this
/// becomes <c>MigrateAsync</c> plus a checked-in migration history.
/// </remarks>
public sealed class DatabaseInitializer(
IServiceScopeFactory scopeFactory,
ILogger<DatabaseInitializer> logger) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
await using var scope = scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<LibraryDbContext>();
var created = await dbContext.Database.EnsureCreatedAsync(cancellationToken);
logger.LogInformation("Library database ready (created: {Created})", created);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
using PLib.Application.Abstractions;
using PLib.Domain.Videos;
namespace PLib.Infrastructure.Persistence;
/// <inheritdoc cref="IVideoRepository"/>
public sealed class EfVideoRepository(LibraryDbContext dbContext) : IVideoRepository
{
public async Task<IReadOnlyList<VideoItem>> GetAllAsync(CancellationToken cancellationToken = default) =>
await dbContext.Videos
.OrderByDescending(x => x.AddedAt)
.ToListAsync(cancellationToken);
public Task<VideoItem?> FindByPathAsync(string fullPath, CancellationToken cancellationToken = default) =>
dbContext.Videos.FirstOrDefaultAsync(x => x.FullPath == fullPath, cancellationToken);
public async Task AddAsync(VideoItem item, CancellationToken cancellationToken = default) =>
await dbContext.Videos.AddAsync(item, cancellationToken);
public Task RemoveAsync(VideoItem item, CancellationToken cancellationToken = default)
{
dbContext.Videos.Remove(item);
return Task.CompletedTask;
}
public Task SaveChangesAsync(CancellationToken cancellationToken = default) =>
dbContext.SaveChangesAsync(cancellationToken);
}
@@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore;
using PLib.Domain.Videos;
namespace PLib.Infrastructure.Persistence;
public sealed class LibraryDbContext(DbContextOptions<LibraryDbContext> options) : DbContext(options)
{
public DbSet<VideoItem> Videos => Set<VideoItem>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(LibraryDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
}
@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PLib.Domain.Videos;
namespace PLib.Infrastructure.Persistence;
internal sealed class VideoItemConfiguration : IEntityTypeConfiguration<VideoItem>
{
/// <summary>
/// SQLite refuses to ORDER BY a DateTimeOffset because its default TEXT representation
/// carries an offset and therefore does not sort chronologically. Storing UTC ticks keeps
/// the column both sortable and indexable.
/// </summary>
private static readonly ValueConverter<DateTimeOffset, long> UtcTicksConverter = new(
value => value.UtcTicks,
ticks => new DateTimeOffset(ticks, TimeSpan.Zero));
public void Configure(EntityTypeBuilder<VideoItem> builder)
{
builder.ToTable("Videos");
builder.HasKey(x => x.Id);
builder.Property(x => x.AddedAt).HasConversion(UtcTicksConverter);
builder.Property(x => x.FileModifiedAt).HasConversion(UtcTicksConverter);
builder.Property(x => x.FullPath)
.IsRequired()
.HasMaxLength(1024);
builder.HasIndex(x => x.FullPath)
.IsUnique();
builder.Property(x => x.Title)
.IsRequired()
.HasMaxLength(512);
builder.Property(x => x.VideoCodec)
.HasMaxLength(64);
builder.Property(x => x.ThumbnailPath)
.HasMaxLength(1024);
// Sorting the grid by "recently added" is the default view, so it gets an index.
builder.HasIndex(x => x.AddedAt);
// IsIndexed is derived from other columns and must not become a table column.
builder.Ignore(x => x.IsIndexed);
}
}