Update README.md with project details, features, requirements, architecture, and data management for PLib video library manager.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user