chore: lots of chores
This commit is contained in:
@@ -5,6 +5,10 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- src/StreamShorts.Console/**
|
- src/StreamShorts.Console/**
|
||||||
- src/StreamShorts.Console.Tests/**
|
- src/StreamShorts.Console.Tests/**
|
||||||
|
- src/StreamShorts.Library/**
|
||||||
|
- src/StreamShorts.Library.Tests/**
|
||||||
|
- StreamShorts.sln
|
||||||
|
- Directory.Build.props
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
using StreamShorts.Library.Media.Video;
|
|
||||||
|
|
||||||
namespace StreamShorts.Console.Commands;
|
namespace StreamShorts.Console.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default command for processing video streams to create short clips.
|
||||||
|
/// </summary>
|
||||||
internal sealed class DefaultCommand(
|
internal sealed class DefaultCommand(
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IAnsiConsole console,
|
IAnsiConsole console,
|
||||||
@@ -28,8 +27,14 @@ internal sealed class DefaultCommand(
|
|||||||
private readonly IShortsCreator _shortsCreator = shortsCreator ?? throw new ArgumentNullException(nameof(shortsCreator));
|
private readonly IShortsCreator _shortsCreator = shortsCreator ?? throw new ArgumentNullException(nameof(shortsCreator));
|
||||||
private readonly TimeProvider _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
private readonly TimeProvider _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the settings for the default command.
|
||||||
|
/// </summary>
|
||||||
internal class Settings : CommandSettings
|
internal class Settings : CommandSettings
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to the stream.
|
||||||
|
/// </summary>
|
||||||
[CommandArgument(0, "[Stream]")]
|
[CommandArgument(0, "[Stream]")]
|
||||||
[Description("The path to the stream")]
|
[Description("The path to the stream")]
|
||||||
public string Stream { get; init; } = string.Empty;
|
public string Stream { get; init; } = string.Empty;
|
||||||
@@ -103,7 +108,7 @@ internal sealed class DefaultCommand(
|
|||||||
);
|
);
|
||||||
|
|
||||||
_fileSystem.Directory.CreateDirectory(outputDirectoryPath);
|
_fileSystem.Directory.CreateDirectory(outputDirectoryPath);
|
||||||
|
|
||||||
await _fileSystem.File.WriteAllTextAsync(
|
await _fileSystem.File.WriteAllTextAsync(
|
||||||
_fileSystem.Path.Combine(outputDirectoryPath, "transcription.txt"),
|
_fileSystem.Path.Combine(outputDirectoryPath, "transcription.txt"),
|
||||||
string.Join(Environment.NewLine, transcriptionSegments)
|
string.Join(Environment.NewLine, transcriptionSegments)
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
namespace StreamShorts.Console.Hosting;
|
namespace StreamShorts.Console.Hosting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for building command applications from host builders.
|
||||||
|
/// </summary>
|
||||||
internal static class HostBuilderExtensions
|
internal static class HostBuilderExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a command application from the host builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The host builder.</param>
|
||||||
|
/// <returns>A configured command application.</returns>
|
||||||
public static CommandApp<DefaultCommand> BuildApp(this IHostBuilder builder)
|
public static CommandApp<DefaultCommand> BuildApp(this IHostBuilder builder)
|
||||||
{
|
{
|
||||||
var registrar = new TypeRegistrar(builder);
|
var registrar = new TypeRegistrar(builder);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
namespace StreamShorts.Console.Hosting;
|
namespace StreamShorts.Console.Hosting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides type registration services for the dependency injection container.
|
||||||
|
/// </summary>
|
||||||
|
/// <inheritdoc/>
|
||||||
internal sealed class TypeRegistrar(IHostBuilder builder) : ITypeRegistrar
|
internal sealed class TypeRegistrar(IHostBuilder builder) : ITypeRegistrar
|
||||||
{
|
{
|
||||||
private readonly IHostBuilder _builder = builder;
|
private readonly IHostBuilder _builder = builder;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
namespace StreamShorts.Console.Hosting;
|
namespace StreamShorts.Console.Hosting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides type resolution services using the dependency injection container.
|
||||||
|
/// </summary>
|
||||||
|
/// <inheritdoc/>
|
||||||
internal sealed class TypeResolver(IHost provider) : ITypeResolver, IDisposable
|
internal sealed class TypeResolver(IHost provider) : ITypeResolver, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IHost _host = provider ?? throw new ArgumentNullException(nameof(provider));
|
private readonly IHost _host = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
|
||||||
using StreamShorts.Library.Media.Video;
|
|
||||||
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
|
||||||
.WriteTo.File(
|
.WriteTo.File(
|
||||||
formatter: new CompactJsonFormatter(),
|
formatter: new CompactJsonFormatter(),
|
||||||
path: Path.Combine(AppContext.BaseDirectory, "logs", "log.jsonl"),
|
path: Path.Combine(AppContext.BaseDirectory, "logs", "log.jsonl"),
|
||||||
@@ -42,7 +38,7 @@ try
|
|||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{keyOptionName} is not configured in appsettings.json.");
|
throw new InvalidOperationException($"{keyOptionName} is not configured in appsettings.json.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientFactory = sp.GetRequiredService<IHttpClientFactory>();
|
var clientFactory = sp.GetRequiredService<IHttpClientFactory>();
|
||||||
|
|
||||||
return new GeminiAnalyzer(clientFactory, key, model);
|
return new GeminiAnalyzer(clientFactory, key, model);
|
||||||
@@ -62,4 +58,4 @@ catch (Exception ex)
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await Log.CloseAndFlushAsync();
|
await Log.CloseAndFlushAsync();
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
<AssemblyTitle>StreamShorts.Console</AssemblyTitle>
|
||||||
|
<Product>StreamShorts.Console</Product>
|
||||||
|
<Description>A command-line interface for StreamShorts</Description>
|
||||||
|
<Version>0.0.0</Version>
|
||||||
|
<Authors>Stevan Freeborn</Authors>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
global using System.ComponentModel;
|
global using System.ComponentModel;
|
||||||
global using System.IO.Abstractions;
|
global using System.IO.Abstractions;
|
||||||
global using System.Reflection;
|
global using System.Reflection;
|
||||||
|
global using System.Text.Json;
|
||||||
|
|
||||||
|
global using Microsoft.Extensions.Configuration;
|
||||||
global using Microsoft.Extensions.DependencyInjection;
|
global using Microsoft.Extensions.DependencyInjection;
|
||||||
global using Microsoft.Extensions.Hosting;
|
global using Microsoft.Extensions.Hosting;
|
||||||
global using Microsoft.Extensions.Logging;
|
global using Microsoft.Extensions.Logging;
|
||||||
@@ -18,4 +20,5 @@ global using StreamShorts.Console.Hosting;
|
|||||||
global using StreamShorts.Library.Analysis;
|
global using StreamShorts.Library.Analysis;
|
||||||
global using StreamShorts.Library.Analysis.Gemini;
|
global using StreamShorts.Library.Analysis.Gemini;
|
||||||
global using StreamShorts.Library.Media.Audio;
|
global using StreamShorts.Library.Media.Audio;
|
||||||
global using StreamShorts.Library.Transcription;
|
global using StreamShorts.Library.Media.Video;
|
||||||
|
global using StreamShorts.Library.Transcription;
|
||||||
@@ -28,4 +28,4 @@ public sealed class FailedTranscriptAnalysisException : Exception
|
|||||||
public FailedTranscriptAnalysisException(string message, Exception innerException) : base(message, innerException)
|
public FailedTranscriptAnalysisException(string message, Exception innerException) : base(message, innerException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents content with role and parts for Gemini API.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Role">The role of the content (e.g., "user", "assistant").</param>
|
||||||
|
/// <param name="Parts">The array of content parts.</param>
|
||||||
internal record Content(
|
internal record Content(
|
||||||
[property: JsonPropertyName("role")]
|
[property: JsonPropertyName("role")]
|
||||||
string Role,
|
string Role,
|
||||||
@@ -7,6 +12,10 @@ internal record Content(
|
|||||||
Part[] Parts
|
Part[] Parts
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a part of content for Gemini API.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Text">The text content of the part.</param>
|
||||||
internal record Part(
|
internal record Part(
|
||||||
[property: JsonPropertyName("text")]
|
[property: JsonPropertyName("text")]
|
||||||
string Text
|
string Text
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace StreamShorts.Library.Analysis.Gemini;
|
namespace StreamShorts.Library.Analysis.Gemini;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to generate content using Gemini.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Contents">The content array for the request.</param>
|
||||||
|
/// <param name="GenerationConfig">The generation configuration.</param>
|
||||||
internal record GenerateContentRequest(
|
internal record GenerateContentRequest(
|
||||||
[property: JsonPropertyName("contents")]
|
[property: JsonPropertyName("contents")]
|
||||||
Content[] Contents,
|
Content[] Contents,
|
||||||
@@ -9,6 +14,10 @@ internal record GenerateContentRequest(
|
|||||||
GenerationConfig GenerationConfig
|
GenerationConfig GenerationConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents configuration for content generation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ResponseMimeType">The MIME type for the response.</param>
|
||||||
internal record GenerationConfig(
|
internal record GenerationConfig(
|
||||||
[property: JsonPropertyName("responseMimeType")]
|
[property: JsonPropertyName("responseMimeType")]
|
||||||
string ResponseMimeType
|
string ResponseMimeType
|
||||||
|
|||||||
@@ -2,13 +2,20 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace StreamShorts.Library.Analysis.Gemini;
|
namespace StreamShorts.Library.Analysis.Gemini;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a response from the Gemini content generation API.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Candidates">The array of candidate responses.</param>
|
||||||
internal record GenerateContentResponse(
|
internal record GenerateContentResponse(
|
||||||
[property: JsonPropertyName("candidates")]
|
[property: JsonPropertyName("candidates")]
|
||||||
Candidate[] Candidates
|
Candidate[] Candidates
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a candidate response from content generation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Content">The generated content.</param>
|
||||||
internal record Candidate(
|
internal record Candidate(
|
||||||
[property: JsonPropertyName("content")]
|
[property: JsonPropertyName("content")]
|
||||||
Content Content
|
Content Content
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -16,4 +16,4 @@ public record ShortCandidate(
|
|||||||
TimeSpan StartTime,
|
TimeSpan StartTime,
|
||||||
[property: JsonPropertyName("end_time")]
|
[property: JsonPropertyName("end_time")]
|
||||||
TimeSpan EndTime
|
TimeSpan EndTime
|
||||||
);
|
);
|
||||||
@@ -16,10 +16,10 @@ public sealed class AudioExtractor : IAudioExtractor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioExtractor"/> class with a specified FFMpeg service.
|
/// Initializes a new instance of the <see cref="AudioExtractor"/> class with a specified <see cref="IVideoService"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="videoService">The video service to use for audio extraction.</param>
|
/// <param name="videoService">The video service to use for audio extraction.</param>
|
||||||
/// <exception cref="ArgumentNullException">Thrown when the FFMpeg service is null.</exception>
|
/// <exception cref="ArgumentNullException">Thrown when the video service is null.</exception>
|
||||||
internal AudioExtractor(IVideoService videoService)
|
internal AudioExtractor(IVideoService videoService)
|
||||||
{
|
{
|
||||||
_videoService = videoService ?? throw new ArgumentNullException(nameof(videoService), $"{nameof(videoService)} cannot be null");
|
_videoService = videoService ?? throw new ArgumentNullException(nameof(videoService), $"{nameof(videoService)} cannot be null");
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ internal sealed class FFMpegService : IVideoService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateClipFromVideoAsync(
|
public async Task CreateClipFromVideoAsync(
|
||||||
string sourcePath,
|
string sourcePath,
|
||||||
string destinationPath,
|
string destinationPath,
|
||||||
TimeSpan startTime,
|
TimeSpan startTime,
|
||||||
TimeSpan endTime,
|
TimeSpan endTime,
|
||||||
TimeSpan? buffer = null
|
TimeSpan? buffer = null
|
||||||
@@ -32,7 +32,7 @@ internal sealed class FFMpegService : IVideoService
|
|||||||
{
|
{
|
||||||
var start = startTime - (buffer ?? TimeSpan.Zero);
|
var start = startTime - (buffer ?? TimeSpan.Zero);
|
||||||
var end = endTime + (buffer ?? TimeSpan.Zero);
|
var end = endTime + (buffer ?? TimeSpan.Zero);
|
||||||
|
|
||||||
await FFMpeg.SubVideoAsync(sourcePath, destinationPath, start, end)
|
await FFMpeg.SubVideoAsync(sourcePath, destinationPath, start, end)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ internal interface IVideoService
|
|||||||
/// <param name="audio">The output audio stream.</param>
|
/// <param name="audio">The output audio stream.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation. The task result indicates whether the extraction was successful.</returns>
|
/// <returns>A task that represents the asynchronous operation. The task result indicates whether the extraction was successful.</returns>
|
||||||
Task<bool> ExtractAudioFromVideoAsync(Stream video, Stream audio);
|
Task<bool> ExtractAudioFromVideoAsync(Stream video, Stream audio);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a clip from a video file based on the specified start and end times.
|
/// Creates a clip from a video file based on the specified start and end times.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -6,14 +6,22 @@ namespace StreamShorts.Library.Media.Video;
|
|||||||
/// Represents a service that creates video shorts.
|
/// Represents a service that creates video shorts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public class ShortsCreator : IShortsCreator
|
public sealed class ShortsCreator : IShortsCreator
|
||||||
{
|
{
|
||||||
private readonly IVideoService _videoService = new FFMpegService();
|
private readonly IVideoService _videoService = new FFMpegService();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ShortsCreator"/> class.
|
||||||
|
/// </summary>
|
||||||
public ShortsCreator()
|
public ShortsCreator()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ShortsCreator"/> class with a specified <see cref="IVideoService"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="videoService">The video service.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when the video service is null.</exception>
|
||||||
internal ShortsCreator(IVideoService videoService)
|
internal ShortsCreator(IVideoService videoService)
|
||||||
{
|
{
|
||||||
_videoService = videoService ?? throw new ArgumentNullException(nameof(videoService), $"{nameof(videoService)} cannot be null");
|
_videoService = videoService ?? throw new ArgumentNullException(nameof(videoService), $"{nameof(videoService)} cannot be null");
|
||||||
@@ -25,17 +33,17 @@ public class ShortsCreator : IShortsCreator
|
|||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(sourcePath), $"{nameof(sourcePath)} cannot be null or whitespace");
|
throw new ArgumentNullException(nameof(sourcePath), $"{nameof(sourcePath)} cannot be null or whitespace");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidate is null)
|
if (candidate is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(candidate), $"{nameof(candidate)} cannot be null");
|
throw new ArgumentNullException(nameof(candidate), $"{nameof(candidate)} cannot be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(destinationPath))
|
if (string.IsNullOrWhiteSpace(destinationPath))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(destinationPath), $"{nameof(destinationPath)} cannot be null or whitespace");
|
throw new ArgumentNullException(nameof(destinationPath), $"{nameof(destinationPath)} cannot be null or whitespace");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _videoService.CreateClipFromVideoAsync(sourcePath, destinationPath, candidate.StartTime, candidate.EndTime, buffer)
|
await _videoService.CreateClipFromVideoAsync(sourcePath, destinationPath, candidate.StartTime, candidate.EndTime, buffer)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ public interface ITranscriber
|
|||||||
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
|
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
|
||||||
/// <returns>An <see cref="IAsyncEnumerable{T}"/> where T is <see cref="TranscriptionSegment"/>.</returns>
|
/// <returns>An <see cref="IAsyncEnumerable{T}"/> where T is <see cref="TranscriptionSegment"/>.</returns>
|
||||||
IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(Stream audio, CancellationToken cancellationToken = default);
|
IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(Stream audio, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace StreamShorts.Console.Tests.Unit.Hosting;
|
namespace StreamShorts.Console.Tests.Unit.Hosting;
|
||||||
|
|
||||||
public class TypeRegistrarTests
|
internal class TypeRegistrarTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_WhenCalled_ItShouldNotThrowShould()
|
public void Constructor_WhenCalled_ItShouldNotThrowShould()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace StreamShorts.Console.Tests.Unit.Hosting;
|
namespace StreamShorts.Console.Tests.Unit.Hosting;
|
||||||
|
|
||||||
public class TypeResolverTests
|
internal class TypeResolverTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_WhenCalledWithNullHost_ItShouldThrowArgumentNullException()
|
public void Constructor_WhenCalledWithNullHost_ItShouldThrowArgumentNullException()
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ global using Microsoft.Extensions.Hosting;
|
|||||||
|
|
||||||
global using Moq;
|
global using Moq;
|
||||||
|
|
||||||
global using StreamShorts.Console.Hosting;
|
global using StreamShorts.Console.Hosting;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace StreamShorts.Library.Tests.Integration.Media.Audio;
|
namespace StreamShorts.Library.Tests.Integration.Media.Audio;
|
||||||
|
|
||||||
public class AudioExtractorTests
|
internal class AudioExtractorTests
|
||||||
{
|
{
|
||||||
private readonly AudioExtractor _sut = new();
|
private readonly AudioExtractor _sut = new();
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using StreamShorts.Library.Media;
|
|||||||
|
|
||||||
namespace StreamShorts.Library.Tests.Unit.Media.Audio;
|
namespace StreamShorts.Library.Tests.Unit.Media.Audio;
|
||||||
|
|
||||||
public class AudioExtractorTests
|
internal class AudioExtractorTests
|
||||||
{
|
{
|
||||||
private readonly Mock<IVideoService> _mockFfmpegService = new();
|
private readonly Mock<IVideoService> _mockFfmpegService = new();
|
||||||
private readonly AudioExtractor _sut;
|
private readonly AudioExtractor _sut;
|
||||||
|
|||||||
Reference in New Issue
Block a user