Add queued FFmpeg conversion and finish splitting the window from the host.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 01:49:50 +02:00
parent 48d03f794f
commit 6fc7506eb7
85 changed files with 3394 additions and 512 deletions

View File

@@ -395,6 +395,48 @@ public class TransferQueueTests
await ctx.Queue.StopAsync(CancellationToken.None);
}
[Fact]
public async Task Convert_fails_when_ffmpeg_is_missing()
{
await using var ctx = await Harness.CreateAsync();
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
await ctx.Queue.StartAsync(CancellationToken.None);
await ops.ConvertAsync(ctx.File("a.txt"), Path.Combine(ctx.Dest, "a.mp4"), ConversionKind.VideoToMp4);
await WaitUntil(() => ctx.Queue.Snapshot().Any(j =>
j.Op == TransferOp.Convert && j.Status == TransferStatus.Failed));
var job = ctx.Queue.Snapshot().Single(j => j.Op == TransferOp.Convert);
Assert.Contains("FFmpeg", job.Error, StringComparison.OrdinalIgnoreCase);
await ctx.Queue.StopAsync(CancellationToken.None);
}
[Fact]
public async Task Fake_convert_writes_output()
{
var fake = new FakeConversionExecutor();
await using var ctx = await Harness.CreateAsync(conversion: fake);
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
await ctx.Queue.StartAsync(CancellationToken.None);
var dest = Path.Combine(ctx.Dest, "a.mp4");
await ops.ConvertAsync(ctx.File("a.txt"), dest, ConversionKind.VideoToMp4);
await WaitUntil(() => ctx.Queue.Snapshot().Any(j =>
j.Op == TransferOp.Convert && j.Status == TransferStatus.Done));
Assert.True(File.Exists(dest));
await ctx.Queue.StopAsync(CancellationToken.None);
}
[Fact]
public async Task Convert_refuses_online_only_cloud_files()
{
await using var ctx = await Harness.CreateAsync(hydration: new AlwaysHydrate(), conversion: new FakeConversionExecutor());
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
await ctx.Queue.StartAsync(CancellationToken.None);
await ops.ConvertAsync(ctx.File("a.txt"), Path.Combine(ctx.Dest, "a.mp4"), ConversionKind.VideoToMp4);
await WaitUntil(() => ctx.Queue.Snapshot().Any(j =>
j.Op == TransferOp.Convert && j.Status == TransferStatus.Failed));
Assert.Equal(FileOperationErrors.CloudHydration, ctx.Queue.Snapshot().Single().Error);
await ctx.Queue.StopAsync(CancellationToken.None);
}
[Fact]
public async Task Empty_recycle_bin_goes_through_the_queue()
{
@@ -438,7 +480,8 @@ public class TransferQueueTests
public static async Task<Harness> CreateAsync(
IArchiveExecutor? archives = null,
IHydrationGuard? hydration = null)
IHydrationGuard? hydration = null,
IMediaConversionProvider? conversion = null)
{
var root = Path.Combine(Path.GetTempPath(), "ew-xfer", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
@@ -452,7 +495,7 @@ public class TransferQueueTests
var shell = new GateShell();
var volumes = new ControlledVolumes();
var queue = new TransferQueue(
new NativeFileOperationExecutor(shell, new DiskEnum(), archives, hydration),
new NativeFileOperationExecutor(shell, new DiskEnum(), archives, hydration, conversion),
store,
volumes,
NullLogger<TransferQueue>.Instance);
@@ -629,6 +672,24 @@ internal sealed class FakeArchiveExecutor : IArchiveExecutor
=> Task.CompletedTask;
}
internal sealed class FakeConversionExecutor : IMediaConversionProvider
{
public bool IsAvailable { get; set; } = true;
public string MissingHint => FfmpegLocator.MissingHint;
public async Task ConvertAsync(
string sourcePath,
string destinationPath,
ConversionKind kind,
IProgress<ConversionProgress>? progress,
CancellationToken cancellationToken)
{
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
await File.WriteAllTextAsync(destinationPath, "converted:" + kind, cancellationToken).ConfigureAwait(false);
progress?.Report(new ConversionProgress(100, destinationPath));
}
}
internal sealed class AlwaysHydrate : IHydrationGuard
{
public bool WouldHydrateOnRead(FileSystemItem item) => true;