Directory.Packages.props
+4
-0
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 227a5cf..5c24826 100644
@@ -18,6 +18,10 @@
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.5.2" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.5.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="11.0.0-preview.3.26207.106" />
<PackageVersion
Include="Microsoft.Extensions.Hosting.Abstractions"
Version="11.0.0-preview.3.26207.106"
/>
<PackageVersion Include="Microsoft.Extensions.Http" Version="11.0.0-preview.3.26207.106" />
<PackageVersion
Include="Microsoft.Extensions.Logging.Abstractions"
src/Cli/Program.cs
+9
-13
diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs
index 0e0bca6..297b471 100644
@@ -1,5 +1,4 @@
using System;
using System.Threading;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Slopper.Domain;
@@ -9,19 +8,16 @@ using Slopper.Infrastructure.Ffmpeg;
var builder = Host.CreateApplicationBuilder();
builder.Services.AddClipSelector();
builder.Services.AddClipSelector().AddClipGenerator();
builder.Services.AddJellyfinDatabase().AddFfmpegServices().AddAi();
builder.Services.AddJellyfinDatabase().AddSlopperDatabase().AddFfmpegServices().AddAi();
using var app = builder.Build();
var media = new MediaItem("test", args[0], new Subtitles.Embedded(0));
await app.StartAsync(CancellationToken.None);
var clipExtractor = app.Services.GetRequiredService<IClipExtractor>();
await clipExtractor.ExtractClip(
media,
TimeSpan.FromSeconds(11),
TimeSpan.FromSeconds(2),
args[1],
CancellationToken.None
);
using var scope = app.Services.CreateScope();
var clipGenerator = scope.ServiceProvider.GetRequiredService<ClipGenerator>();
await clipGenerator.Generate(CancellationToken.None);
await app.StopAsync(CancellationToken.None);
src/Cli/packages.lock.json
+20
-12
diff --git a/src/Cli/packages.lock.json b/src/Cli/packages.lock.json
index b785765..5238b59 100644
@@ -38,6 +38,12 @@
"Microsoft.Extensions.Options": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[11.0.0-preview.2.26159.112, )",
"resolved": "11.0.0-preview.2.26159.112",
"contentHash": "TnwQtmgvXJ+Moj8F0X41gNiLvXvBjYuWycwC0BUlYM3NHP0jzBAe8jWGuxlPNxh/XGDA7oOcwcpTLGcVJNwOYQ=="
},
"Instances": {
"type": "Transitive",
"resolved": "3.0.2",
@@ -255,18 +261,6 @@
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "gI8O5FzTgw9yKbYKvGxDdymIackACfG+VF5cAisZExZcZ3/BaZ1YBN7jsURoiHUmaN8KTNwCqjxWhITHFq18Cw=="
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "iPci8e1kji1I1htDLS73y7+AnYIEneWzswjcijaR1Yl/Gc7HAEdx9SZTpt77T8TB9c9ejHiMazzIjlnXm4G18A==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Diagnostics.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.FileProviders.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Logging.Abstractions": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
@@ -417,6 +411,7 @@
"dependencies": {
"Jellyfin.Database.Implementations": "[10.11.8, )",
"Microsoft.EntityFrameworkCore.Sqlite": "[10.0.7, )",
"Microsoft.Extensions.Hosting.Abstractions": "[11.0.0-preview.3.26207.106, )",
"Slopper.Domain": "[1.0.0, )"
}
},
@@ -482,6 +477,19 @@
"resolved": "10.5.2",
"contentHash": "Ei+YWV9Ybnps7pR1dgjlG29gelXEwZkhLVAcWmKe6HvXS6LNBYgSdWiY3Hk9OZXYtK34rv/NtLWBQYQGOBQYPQ=="
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.3.26207.106, )",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "iPci8e1kji1I1htDLS73y7+AnYIEneWzswjcijaR1Yl/Gc7HAEdx9SZTpt77T8TB9c9ejHiMazzIjlnXm4G18A==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Diagnostics.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.FileProviders.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Logging.Abstractions": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.Extensions.Http": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.3.26207.106, )",
src/Domain/Clip.cs
+12
-0
diff --git a/src/Domain/Clip.cs b/src/Domain/Clip.cs
new file mode 100644
index 0000000..8fca7bf
@@ -0,0 +1,12 @@
using System;
namespace Slopper.Domain;
public sealed record Clip(
Guid Id,
Guid MediaItemId,
string Path,
TimeSpan Start,
TimeSpan Duration,
DateTimeOffset CreatedAt
);
src/Domain/ClipGenerator.cs
+91
-0
diff --git a/src/Domain/ClipGenerator.cs b/src/Domain/ClipGenerator.cs
new file mode 100644
index 0000000..8d438b9
@@ -0,0 +1,91 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace Slopper.Domain;
public sealed class ClipGenerator(
IOptions<ClipGeneratorOptions> options,
TimeProvider timeProvider,
IMediaRepository mediaRepository,
ClipSelector clipSelector,
IClipExtractor clipExtractor,
IClipRepository clipRepository
)
{
private readonly string clipDirectory = options.Value.ClipDirectory;
public async Task<Clip> Generate(CancellationToken cancellationToken)
{
var media = await mediaRepository.GetRandomMediaItem(cancellationToken);
var (start, duration) = await clipSelector.PickClip(media, cancellationToken);
var utcNow = timeProvider.GetUtcNow();
var clipId = Guid.CreateVersion7();
var clipPath = Path.Join(clipDirectory, $"{clipId}.mp4");
await clipExtractor.ExtractClip(media, start, duration, clipPath, cancellationToken);
var clip = new Clip(clipId, media.Id, clipPath, start, duration, utcNow);
await clipRepository.Save(clip, cancellationToken);
return clip;
}
}
public static class ClipGeneratorServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddClipGenerator()
{
services.AddOptions<ClipGeneratorOptions>().BindConfiguration("").ValidateOnStart();
services.AddTransient<IValidateOptions<ClipGeneratorOptions>, ClipGeneratorOptionsValidator>();
services.TryAddSingleton(TimeProvider.System);
services.AddTransient<ClipGenerator>();
return services;
}
}
}
public sealed class ClipGeneratorOptions
{
public required string ClipDirectory { get; set; }
}
public sealed class ClipGeneratorOptionsValidator : IValidateOptions<ClipGeneratorOptions>
{
public ValidateOptionsResult Validate(string? name, ClipGeneratorOptions options)
{
var builder = new ValidateOptionsResultBuilder();
ValidateClipDirectory(name, options.ClipDirectory, builder);
return builder.Build();
}
private static void ValidateClipDirectory(string? name, string clipDirectory, ValidateOptionsResultBuilder builder)
{
string propertyName = string.IsNullOrEmpty(name)
? nameof(ClipGeneratorOptions.ClipDirectory)
: $"{name}.{nameof(ClipGeneratorOptions.ClipDirectory)}";
if (string.IsNullOrWhiteSpace(clipDirectory))
{
builder.AddError("Cannot be null or empty.", propertyName);
return;
}
if (!Directory.Exists(clipDirectory))
{
builder.AddError("Directory must exist.", propertyName);
return;
}
}
}
src/Domain/ClipSelector.cs
+4
-1
diff --git a/src/Domain/ClipSelector.cs b/src/Domain/ClipSelector.cs
index 0894945..f37034e 100644
@@ -11,6 +11,7 @@ using Microsoft.Extensions.Options;
namespace Slopper.Domain;
public sealed class ClipSelector(
ISubtitleReader subtitleReader,
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
IOptions<ClipSelectorOptions> options
)
@@ -20,10 +21,12 @@ public sealed class ClipSelector(
);
public async Task<(TimeSpan start, TimeSpan duration)> PickClip(
IEnumerable<SubtitleEntry> subtitles,
MediaItem media,
CancellationToken cancellationToken
)
{
var subtitles = await subtitleReader.ReadSubtitles(media, cancellationToken);
var subtitleLines = subtitles.SelectMany(s => s.Lines);
var subtitleEmbeddings = await embeddingGenerator.GenerateAsync(
subtitleLines,
src/Domain/IClipRepository.cs
+9
-0
diff --git a/src/Domain/IClipRepository.cs b/src/Domain/IClipRepository.cs
new file mode 100644
index 0000000..11ee6ab
@@ -0,0 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
namespace Slopper.Domain;
public interface IClipRepository
{
Task Save(Clip clip, CancellationToken cancellationToken);
}
src/Domain/MediaItem.cs
+3
-1
diff --git a/src/Domain/MediaItem.cs b/src/Domain/MediaItem.cs
index e5ff6b9..2d820bc 100644
@@ -1,3 +1,5 @@
using System;
namespace Slopper.Domain;
public sealed record MediaItem(string Name, string Path, Subtitles Subtitles);
public sealed record MediaItem(Guid Id, string Path, Subtitles Subtitles);
src/Domain/packages.lock.json
+6
-0
diff --git a/src/Domain/packages.lock.json b/src/Domain/packages.lock.json
index a63ab9a..9890edc 100644
@@ -46,6 +46,12 @@
"Microsoft.Extensions.Primitives": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[11.0.0-preview.2.26159.112, )",
"resolved": "11.0.0-preview.2.26159.112",
"contentHash": "TnwQtmgvXJ+Moj8F0X41gNiLvXvBjYuWycwC0BUlYM3NHP0jzBAe8jWGuxlPNxh/XGDA7oOcwcpTLGcVJNwOYQ=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
src/Infrastructure/Ai/packages.lock.json
+6
-0
diff --git a/src/Infrastructure/Ai/packages.lock.json b/src/Infrastructure/Ai/packages.lock.json
index 17984d4..a2eb8c9 100644
@@ -35,6 +35,12 @@
"Microsoft.Extensions.Options": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[11.0.0-preview.2.26159.112, )",
"resolved": "11.0.0-preview.2.26159.112",
"contentHash": "TnwQtmgvXJ+Moj8F0X41gNiLvXvBjYuWycwC0BUlYM3NHP0jzBAe8jWGuxlPNxh/XGDA7oOcwcpTLGcVJNwOYQ=="
},
"OllamaSharp": {
"type": "Direct",
"requested": "[5.4.25, )",
src/Infrastructure/Database/Database.csproj
+1
-0
diff --git a/src/Infrastructure/Database/Database.csproj b/src/Infrastructure/Database/Database.csproj
index a1e201c..b92bdcd 100644
@@ -13,5 +13,6 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
</ItemGroup>
</Project>
src/Infrastructure/Database/Jellyfin/JellyfinDbContextDesignFactory.cs
+0
-19
diff --git a/src/Infrastructure/Database/Jellyfin/JellyfinDbContextDesignFactory.cs b/src/Infrastructure/Database/Jellyfin/JellyfinDbContextDesignFactory.cs
deleted file mode 100644
index 71056c9..0000000
@@ -1,19 +0,0 @@
using Jellyfin.Database.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging.Abstractions;
namespace Slopper.Infrastructure.Database.Jellyfin;
internal sealed class JellyfinDbContextDesignFactory : IDesignTimeDbContextFactory<JellyfinDbContext>
{
public JellyfinDbContext CreateDbContext(string[] args) =>
new(
new DbContextOptionsBuilder<JellyfinDbContext>()
.UseSqlite(args[0], b => b.MigrationsAssembly("Slopper.Infrastructure.Database"))
.Options,
NullLogger<JellyfinDbContext>.Instance,
new JellyfinDatabaseProvider(),
new EntityFrameworkCoreLockingBehavior()
);
}
src/Infrastructure/Database/Jellyfin/MediaRepository.cs
+1
-14
diff --git a/src/Infrastructure/Database/Jellyfin/MediaRepository.cs b/src/Infrastructure/Database/Jellyfin/MediaRepository.cs
index 32c4115..60bd7d0 100644
@@ -33,20 +33,7 @@ internal sealed class MediaRepository(JellyfinDbContext jellyfinDbContext, Rando
}
private static MediaItem Map(MediaStreamInfo mediaStreamInfo) =>
new(MapName(mediaStreamInfo.Item), MapVideoPath(mediaStreamInfo.Item), MapSubtitle(mediaStreamInfo));
private static string MapName(BaseItemEntity item) =>
item switch
{
{ SeriesName: string seriesName, SeasonName: string seasonName, EpisodeTitle: string episodeTitle } =>
$"{seriesName} {seasonName} {episodeTitle}",
{ SeriesName: string seriesName, SeasonName: string seasonName, Name: string name } =>
$"{seriesName} {seasonName} {name}",
{ Name: string name } => name,
{ SortName: string sortName } => sortName,
{ CleanName: string cleanName } => cleanName,
_ => throw new Exception("No name for media item"),
};
new(mediaStreamInfo.ItemId, MapVideoPath(mediaStreamInfo.Item), MapSubtitle(mediaStreamInfo));
private static string MapVideoPath(BaseItemEntity item) =>
item.Path ?? throw new Exception("No video stream for media item");
src/Infrastructure/Database/ServiceCollectionExtensions.cs
+25
-9
diff --git a/src/Infrastructure/Database/ServiceCollectionExtensions.cs b/src/Infrastructure/Database/ServiceCollectionExtensions.cs
index fdce6d3..f61a648 100644
@@ -7,20 +7,36 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Slopper.Domain;
using Slopper.Infrastructure.Database.Jellyfin;
using Slopper.Infrastructure.Database.Slopper;
namespace Slopper.Infrastructure.Database;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddJellyfinDatabase(this IServiceCollection services)
extension(IServiceCollection services)
{
services.AddSingleton<IJellyfinDatabaseProvider, JellyfinDatabaseProvider>();
services.AddSingleton<IEntityFrameworkCoreLockingBehavior, EntityFrameworkCoreLockingBehavior>();
services.AddDbContext<JellyfinDbContext>(
(sp, options) => options.UseSqlite(sp.GetRequiredService<IConfiguration>().GetConnectionString("jellyfin"))
);
services.TryAddSingleton(Random.Shared);
services.AddTransient<IMediaRepository, MediaRepository>();
return services;
public IServiceCollection AddJellyfinDatabase()
{
services.AddSingleton<IJellyfinDatabaseProvider, JellyfinDatabaseProvider>();
services.AddSingleton<IEntityFrameworkCoreLockingBehavior, EntityFrameworkCoreLockingBehavior>();
services.AddDbContext<JellyfinDbContext>(
(sp, options) =>
options.UseSqlite(sp.GetRequiredService<IConfiguration>().GetConnectionString("jellyfin"))
);
services.TryAddSingleton(Random.Shared);
services.AddTransient<IMediaRepository, MediaRepository>();
return services;
}
public IServiceCollection AddSlopperDatabase()
{
services.AddHostedService<SlopperStartupMigration>();
services.AddDbContext<SlopperDbContext>(
(sp, options) =>
options.UseSqlite(sp.GetRequiredService<IConfiguration>().GetConnectionString("slopper"))
);
services.AddTransient<IClipRepository, ClipRepository>();
return services;
}
}
}
src/Infrastructure/Database/Slopper/ClipRepository.cs
+14
-0
diff --git a/src/Infrastructure/Database/Slopper/ClipRepository.cs b/src/Infrastructure/Database/Slopper/ClipRepository.cs
new file mode 100644
index 0000000..01debb2
@@ -0,0 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
using Slopper.Domain;
namespace Slopper.Infrastructure.Database.Slopper;
internal sealed class ClipRepository(SlopperDbContext slopperContext) : IClipRepository
{
public async Task Save(Clip clip, CancellationToken cancellationToken)
{
slopperContext.Clips.Add(clip);
await slopperContext.SaveChangesAsync(cancellationToken);
}
}
src/Infrastructure/Database/Slopper/Migrations/20260507141946_InitialCreate.Designer.cs
+52
-0
diff --git a/src/Infrastructure/Database/Slopper/Migrations/20260507141946_InitialCreate.Designer.cs b/src/Infrastructure/Database/Slopper/Migrations/20260507141946_InitialCreate.Designer.cs
new file mode 100644
index 0000000..778dd1b
@@ -0,0 +1,52 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Slopper.Infrastructure.Database.Slopper;
#nullable disable
namespace Slopper.Infrastructure.Database.Slopper.Migrations
{
[DbContext(typeof(SlopperDbContext))]
[Migration("20260507141946_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("Slopper.Domain.Clip", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<TimeSpan>("Duration")
.HasColumnType("TEXT");
b.Property<Guid>("MediaItemId")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");
b.Property<TimeSpan>("Start")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Clips");
});
#pragma warning restore 612, 618
}
}
}
src/Infrastructure/Database/Slopper/Migrations/20260507141946_InitialCreate.cs
+38
-0
diff --git a/src/Infrastructure/Database/Slopper/Migrations/20260507141946_InitialCreate.cs b/src/Infrastructure/Database/Slopper/Migrations/20260507141946_InitialCreate.cs
new file mode 100644
index 0000000..65b3fcb
@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Slopper.Infrastructure.Database.Slopper.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Clips",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
MediaItemId = table.Column<Guid>(type: "TEXT", nullable: false),
Path = table.Column<string>(type: "TEXT", nullable: false),
Start = table.Column<TimeSpan>(type: "TEXT", nullable: false),
Duration = table.Column<TimeSpan>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_Clips", x => x.Id);
}
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "Clips");
}
}
}
src/Infrastructure/Database/Slopper/Migrations/SlopperDbContextModelSnapshot.cs
+49
-0
diff --git a/src/Infrastructure/Database/Slopper/Migrations/SlopperDbContextModelSnapshot.cs b/src/Infrastructure/Database/Slopper/Migrations/SlopperDbContextModelSnapshot.cs
new file mode 100644
index 0000000..6ad0f58
@@ -0,0 +1,49 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Slopper.Infrastructure.Database.Slopper;
#nullable disable
namespace Slopper.Infrastructure.Database.Slopper.Migrations
{
[DbContext(typeof(SlopperDbContext))]
partial class SlopperDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("Slopper.Domain.Clip", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<TimeSpan>("Duration")
.HasColumnType("TEXT");
b.Property<Guid>("MediaItemId")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");
b.Property<TimeSpan>("Start")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Clips");
});
#pragma warning restore 612, 618
}
}
}
src/Infrastructure/Database/Slopper/SlopperDbContext.cs
+18
-0
diff --git a/src/Infrastructure/Database/Slopper/SlopperDbContext.cs b/src/Infrastructure/Database/Slopper/SlopperDbContext.cs
new file mode 100644
index 0000000..6edf3b2
@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Slopper.Domain;
namespace Slopper.Infrastructure.Database.Slopper;
internal sealed class SlopperDbContext(DbContextOptions options) : DbContext(options)
{
public required DbSet<Clip> Clips { get; init; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var clipBuilder = modelBuilder.Entity<Clip>();
clipBuilder.HasKey(c => c.Id);
clipBuilder.Property(c => c.MediaItemId);
clipBuilder.Property(c => c.Path);
clipBuilder.Property(c => c.CreatedAt);
}
}
src/Infrastructure/Database/Slopper/SlopperDbContextDesignFactory.cs
+10
-0
diff --git a/src/Infrastructure/Database/Slopper/SlopperDbContextDesignFactory.cs b/src/Infrastructure/Database/Slopper/SlopperDbContextDesignFactory.cs
new file mode 100644
index 0000000..24f2d1d
@@ -0,0 +1,10 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Slopper.Infrastructure.Database.Slopper;
internal sealed class SlopperDbContextDesignFactory : IDesignTimeDbContextFactory<SlopperDbContext>
{
public SlopperDbContext CreateDbContext(string[] args) =>
new(new DbContextOptionsBuilder<SlopperDbContext>().UseSqlite().Options) { Clips = null! };
}
src/Infrastructure/Database/Slopper/SlopperStartupMigration.cs
+19
-0
diff --git a/src/Infrastructure/Database/Slopper/SlopperStartupMigration.cs b/src/Infrastructure/Database/Slopper/SlopperStartupMigration.cs
new file mode 100644
index 0000000..5ba8702
@@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Slopper.Infrastructure.Database.Slopper;
public class SlopperStartupMigration(IServiceScopeFactory scopeFactory) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = scopeFactory.CreateScope();
var slopperDbContext = scope.ServiceProvider.GetRequiredService<SlopperDbContext>();
await slopperDbContext.Database.MigrateAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
src/Infrastructure/Database/packages.lock.json
+36
-0
diff --git a/src/Infrastructure/Database/packages.lock.json b/src/Infrastructure/Database/packages.lock.json
index b50bb48..70b819b 100644
@@ -53,6 +53,25 @@
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Direct",
"requested": "[11.0.0-preview.3.26207.106, )",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "iPci8e1kji1I1htDLS73y7+AnYIEneWzswjcijaR1Yl/Gc7HAEdx9SZTpt77T8TB9c9ejHiMazzIjlnXm4G18A==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Diagnostics.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.FileProviders.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Logging.Abstractions": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[11.0.0-preview.2.26159.112, )",
"resolved": "11.0.0-preview.2.26159.112",
"contentHash": "TnwQtmgvXJ+Moj8F0X41gNiLvXvBjYuWycwC0BUlYM3NHP0jzBAe8jWGuxlPNxh/XGDA7oOcwcpTLGcVJNwOYQ=="
},
"Humanizer.Core": {
"type": "Transitive",
"resolved": "2.14.1",
@@ -246,6 +265,23 @@
"resolved": "10.0.7",
"contentHash": "gCglFg/9Chu3lyJNytRuQAYM3mXQKNs1i01Cz2bc545QaHQ+LbBb4O5UCfu968Gro3ZVSOZ/ktilmPcaUSGSZA=="
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "Jw8scnPDYKkBJE3LSvAQQ/P4OBypQclFuFqcYo3RLGt5zr9EhC1V0ozwxr8/xe/66IHfPA9YhdhYegAn4Y7t5Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Options": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "gFkzHF108G5VcXP2vByvxTEi3ixKn9K5Br+qOXYu+Ezyk6SDOLyl9jryVyEhwcAERzb6/ba3ZEE2gdPcQBbhgA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.7",
src/Infrastructure/Ffmpeg/packages.lock.json
+6
-0
diff --git a/src/Infrastructure/Ffmpeg/packages.lock.json b/src/Infrastructure/Ffmpeg/packages.lock.json
index 72c0e1e..a4a12f4 100644
@@ -26,6 +26,12 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[11.0.0-preview.2.26159.112, )",
"resolved": "11.0.0-preview.2.26159.112",
"contentHash": "TnwQtmgvXJ+Moj8F0X41gNiLvXvBjYuWycwC0BUlYM3NHP0jzBAe8jWGuxlPNxh/XGDA7oOcwcpTLGcVJNwOYQ=="
},
"SubtitlesParserV2": {
"type": "Direct",
"requested": "[2.4.0, )",