Directory.Packages.props
+1
-0
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b45d39b..a6a8c0c 100644
@@ -17,5 +17,6 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="11.0.0-preview.3.26207.106" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="11.0.0-preview.3.26207.106" />
<PackageVersion Include="SubtitlesParserV2" Version="2.4.0" />
</ItemGroup>
</Project>
\ No newline at end of file
src/Cli/Program.cs
+12
-7
diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs
index 710e5a0..9a1df89 100644
@@ -1,8 +1,9 @@
using System;
using System.Reflection;
using System.Reflection;
using System.Threading;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Slopper.Domain;
using Slopper.Infrastructure.Database;
using Slopper.Infrastructure.Ffmpeg;
@@ -15,11 +16,15 @@ builder.Services.AddJellyfinDatabase().AddFfmpegServices();
using var app = builder.Build();
var clipExtractor = app.Services.GetRequiredService<ClipExtractor>();
var subtitleReader = app.Services.GetRequiredService<ISubtitleReader>();
await clipExtractor.Clip(
var lines = await subtitleReader.ReadSubtitles(
new("media", args[0], new Subtitles.Embedded(0)),
TimeSpan.FromMinutes(1),
TimeSpan.FromSeconds(5),
args[1]
CancellationToken.None
);
var logger = app.Services.GetRequiredService<ILogger<Program>>();
foreach (var line in lines)
{
logger.LogInformation("{Start} ({Duration}): {Lines}", line.Start, line.Duration, line.Lines);
}
src/Cli/packages.lock.json
+11
-2
diff --git a/src/Cli/packages.lock.json b/src/Cli/packages.lock.json
index b388142..9ed0590 100644
@@ -413,7 +413,6 @@
"Slopper.Domain": {
"type": "Project",
"dependencies": {
"FFMpegCore": "[5.4.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[11.0.0-preview.3.26207.106, )"
}
},
@@ -430,7 +429,8 @@
"dependencies": {
"FFMpegCore": "[5.4.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[11.0.0-preview.3.26207.106, )",
"Slopper.Domain": "[1.0.0, )"
"Slopper.Domain": "[1.0.0, )",
"SubtitlesParserV2": "[2.4.0, )"
}
},
"FFMpegCore": {
@@ -475,6 +475,15 @@
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106"
}
},
"SubtitlesParserV2": {
"type": "CentralTransitive",
"requested": "[2.4.0, )",
"resolved": "2.4.0",
"contentHash": "/QDneMSeMa1SxLu8IDsaX6WFFDdUqxn7GVI654NGgK/X4nGqKLmlrMi2CUFROD1nphp7EURrlIPIsAgSRtEgcg==",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "9.0.1"
}
}
},
"net11.0/linux-arm64": {
src/Domain/Domain.csproj
+0
-1
diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj
index 1e887de..0c691f1 100644
@@ -4,7 +4,6 @@
<AssemblyName>Slopper.Domain</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FFMpegCore" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
</Project>
src/Domain/ISubtitleLinesReader.cs
+9
-0
diff --git a/src/Domain/ISubtitleLinesReader.cs b/src/Domain/ISubtitleLinesReader.cs
new file mode 100644
index 0000000..2d938dd
@@ -0,0 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
namespace Slopper.Domain;
public interface ISubtitleReader
{
Task<SubtitleEntry[]> ReadSubtitles(MediaItem media, CancellationToken cancellationToken);
}
src/Domain/SubtitleEntry.cs
+6
-0
diff --git a/src/Domain/SubtitleEntry.cs b/src/Domain/SubtitleEntry.cs
new file mode 100644
index 0000000..b0db7b4
@@ -0,0 +1,6 @@
using System;
using System.Collections.Immutable;
namespace Slopper.Domain;
public sealed record SubtitleEntry(IImmutableList<string> Lines, TimeSpan Start, TimeSpan Duration);
src/Domain/packages.lock.json
+0
-14
diff --git a/src/Domain/packages.lock.json b/src/Domain/packages.lock.json
index c408979..2eb7775 100644
@@ -8,15 +8,6 @@
"resolved": "1.2.6",
"contentHash": "KMSJG+jfk7vjP52QkWB99qWespXCPAzG/IaMCMRHYWumJEAGKQYm2HtyWG6eqnOwDitH96i1cqq5EVesyOtPmg=="
},
"FFMpegCore": {
"type": "Direct",
"requested": "[5.4.0, )",
"resolved": "5.4.0",
"contentHash": "nymhPWpwvBaB4fOqf0thxg7Inm8SrruFQLdCMFEDKD8S3hy0/iV2vuKaJvaL9ZnbJx/2rJCHnJXOymo2apXovg==",
"dependencies": {
"Instances": "3.0.2"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Direct",
"requested": "[11.0.0-preview.3.26207.106, )",
@@ -26,11 +17,6 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106"
}
},
"Instances": {
"type": "Transitive",
"resolved": "3.0.2",
"contentHash": "LfhegDpmA8PuHW58RmgVvCDG/mfVCTU+Vhy4ppmXLJfAer33Xl0NocDy92OwSL6CnkVdx41O/I0+BjNhU1JtMQ=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
src/Infrastructure/Database/packages.lock.json
+0
-15
diff --git a/src/Infrastructure/Database/packages.lock.json b/src/Infrastructure/Database/packages.lock.json
index 1a3c727..b116cf1 100644
@@ -58,11 +58,6 @@
"resolved": "2.14.1",
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
},
"Instances": {
"type": "Transitive",
"resolved": "3.0.2",
"contentHash": "LfhegDpmA8PuHW58RmgVvCDG/mfVCTU+Vhy4ppmXLJfAer33Xl0NocDy92OwSL6CnkVdx41O/I0+BjNhU1JtMQ=="
},
"Microsoft.Build.Framework": {
"type": "Transitive",
"resolved": "18.0.2",
@@ -371,19 +366,9 @@
"Slopper.Domain": {
"type": "Project",
"dependencies": {
"FFMpegCore": "[5.4.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[11.0.0-preview.3.26207.106, )"
}
},
"FFMpegCore": {
"type": "CentralTransitive",
"requested": "[5.4.0, )",
"resolved": "5.4.0",
"contentHash": "nymhPWpwvBaB4fOqf0thxg7Inm8SrruFQLdCMFEDKD8S3hy0/iV2vuKaJvaL9ZnbJx/2rJCHnJXOymo2apXovg==",
"dependencies": {
"Instances": "3.0.2"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.3.26207.106, )",
src/Infrastructure/Ffmpeg/Ffmpeg.csproj
+1
-0
diff --git a/src/Infrastructure/Ffmpeg/Ffmpeg.csproj b/src/Infrastructure/Ffmpeg/Ffmpeg.csproj
index 4b5a52f..733ea29 100644
@@ -9,5 +9,6 @@
<ItemGroup>
<PackageReference Include="FFMpegCore" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="SubtitlesParserV2" />
</ItemGroup>
</Project>
src/Infrastructure/Ffmpeg/ServiceCollectionExtensions.cs
+2
-1
diff --git a/src/Infrastructure/Ffmpeg/ServiceCollectionExtensions.cs b/src/Infrastructure/Ffmpeg/ServiceCollectionExtensions.cs
index 08a10d3..85e69fa 100644
@@ -1,9 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
using Slopper.Domain;
namespace Slopper.Infrastructure.Ffmpeg;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddFfmpegServices(this IServiceCollection services) =>
services.AddTransient<ClipExtractor>();
services.AddTransient<ClipExtractor>().AddTransient<ISubtitleReader, SubtitleReader>();
}
src/Infrastructure/Ffmpeg/SubtitleReader.cs
+58
-0
diff --git a/src/Infrastructure/Ffmpeg/SubtitleReader.cs b/src/Infrastructure/Ffmpeg/SubtitleReader.cs
new file mode 100644
index 0000000..f93c807
@@ -0,0 +1,58 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FFMpegCore;
using FFMpegCore.Enums;
using FFMpegCore.Pipes;
using Microsoft.Extensions.Logging;
using Slopper.Domain;
using SubtitlesParserV2;
namespace Slopper.Infrastructure.Ffmpeg;
internal sealed class SubtitleReader(ILogger<SubtitleReader> logger) : ISubtitleReader
{
public async Task<SubtitleEntry[]> ReadSubtitles(MediaItem media, CancellationToken cancellationToken) =>
media.Subtitles switch
{
Subtitles.External(var path) => ReadSubtitles(path),
Subtitles.Embedded(var index) => await ReadSubtitles(media.Path, index),
_ => throw new ArgumentException("Unknown subtitle type"),
};
private static SubtitleEntry[] ReadSubtitles(string path)
{
using var stream = File.OpenRead(path);
return ReadSubtitles(stream);
}
private async Task<SubtitleEntry[]> ReadSubtitles(string path, int index)
{
using var stream = new MemoryStream();
var args = FFMpegArguments
.FromFileInput(path)
.OutputToPipe(
new StreamPipeSink(stream),
addArguments: options => options.SelectStream(index, channel: Channel.Subtitle).ForceFormat("srt")
);
logger.LogInformation("Running ffmpeg {FfmpegArguments}", args.Arguments);
await args.ProcessAsynchronously();
stream.Position = 0;
return ReadSubtitles(stream);
}
private static SubtitleEntry[] ReadSubtitles(Stream stream)
{
var result = SubtitleParser.ParseStream(stream) ?? throw new Exception("Cannot parse subtitles.");
return
[
.. result.Subtitles.Select(s => new SubtitleEntry(
[.. s.Lines],
TimeSpan.FromMilliseconds(s.StartTime),
TimeSpan.FromMilliseconds(s.EndTime - s.StartTime)
)),
];
}
}
src/Infrastructure/Ffmpeg/packages.lock.json
+9
-1
diff --git a/src/Infrastructure/Ffmpeg/packages.lock.json b/src/Infrastructure/Ffmpeg/packages.lock.json
index fc8fffa..6803c40 100644
@@ -26,6 +26,15 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106"
}
},
"SubtitlesParserV2": {
"type": "Direct",
"requested": "[2.4.0, )",
"resolved": "2.4.0",
"contentHash": "/QDneMSeMa1SxLu8IDsaX6WFFDdUqxn7GVI654NGgK/X4nGqKLmlrMi2CUFROD1nphp7EURrlIPIsAgSRtEgcg==",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "9.0.1"
}
},
"Instances": {
"type": "Transitive",
"resolved": "3.0.2",
@@ -39,7 +48,6 @@
"Slopper.Domain": {
"type": "Project",
"dependencies": {
"FFMpegCore": "[5.4.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[11.0.0-preview.3.26207.106, )"
}
}