Initial commit: Futonizer MKV track stripper
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using Futonizer.Models;
|
||||
using Futonizer.Services;
|
||||
using Wpf.Ui.Controls;
|
||||
|
||||
namespace Futonizer.Views;
|
||||
|
||||
public partial class ProgressWindow : FluentWindow
|
||||
{
|
||||
private const int MaxConcurrentStrips = 5;
|
||||
|
||||
private readonly IReadOnlyList<QueuedFileItem> _files;
|
||||
private readonly string _mkvMergePath;
|
||||
private readonly string _outputFolder;
|
||||
private readonly ObservableCollection<JobProgressItem> _activeJobs = new();
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _isDone;
|
||||
|
||||
/// <summary>
|
||||
/// True once processing has finished running (successfully, with failures,
|
||||
/// or cancelled) — i.e. whenever the Cancel button has turned into Close.
|
||||
/// Used by the caller to decide whether to clear its queue after this
|
||||
/// window is closed.
|
||||
/// </summary>
|
||||
public bool ProcessingCompleted { get; private set; }
|
||||
|
||||
public ProgressWindow(IReadOnlyList<QueuedFileItem> files, string mkvMergePath, string outputFolder)
|
||||
{
|
||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this);
|
||||
InitializeComponent();
|
||||
|
||||
_files = files;
|
||||
_mkvMergePath = mkvMergePath;
|
||||
_outputFolder = outputFolder;
|
||||
|
||||
Title = $"Stripping {files.Count} file{(files.Count == 1 ? "" : "s")}";
|
||||
ActiveJobsList.ItemsSource = _activeJobs;
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = StartProcessingAsync();
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
|
||||
private void CancelCloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isDone)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
_cts?.Cancel();
|
||||
CancelCloseButton.IsEnabled = false;
|
||||
StatusText.Text = "Cancelling...";
|
||||
}
|
||||
|
||||
private void AppendLog(string line)
|
||||
{
|
||||
LogBox.AppendText(line + Environment.NewLine);
|
||||
LogBox.ScrollToEnd();
|
||||
}
|
||||
|
||||
private void UpdateTotalSpeed()
|
||||
{
|
||||
if (_activeJobs.Count == 0)
|
||||
{
|
||||
TotalSpeedText.Text = string.Empty;
|
||||
return;
|
||||
}
|
||||
double total = _activeJobs.Sum(j => j.MegabytesPerSecond);
|
||||
TotalSpeedText.Text = _activeJobs.Count == 1
|
||||
? $"{total:F1} MB/s"
|
||||
: $"{total:F1} MB/s across {_activeJobs.Count} file(s)";
|
||||
}
|
||||
|
||||
private async Task StartProcessingAsync()
|
||||
{
|
||||
StatusText.Text = "Stripping tracks...";
|
||||
_cts = new CancellationTokenSource();
|
||||
var ct = _cts.Token;
|
||||
|
||||
bool overwritingInPlace = string.IsNullOrWhiteSpace(_outputFolder);
|
||||
var service = new MkvService(_mkvMergePath);
|
||||
|
||||
AppendLog($"Starting track removal for {_files.Count} file(s) (up to {MaxConcurrentStrips} at a time)...");
|
||||
AppendLog(new string('-', 80));
|
||||
|
||||
int successCount = 0;
|
||||
long totalBytesWritten = 0;
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
using var semaphore = new SemaphoreSlim(MaxConcurrentStrips);
|
||||
|
||||
var stripTasks = _files.Select(async file =>
|
||||
{
|
||||
await semaphore.WaitAsync(ct);
|
||||
|
||||
var job = new JobProgressItem { FileName = file.FileName };
|
||||
job.PropertyChanged += (_, args) =>
|
||||
{
|
||||
if (args.PropertyName == nameof(JobProgressItem.MegabytesPerSecond))
|
||||
UpdateTotalSpeed();
|
||||
};
|
||||
_activeJobs.Add(job);
|
||||
UpdateTotalSpeed();
|
||||
|
||||
try
|
||||
{
|
||||
string outputFilePath = overwritingInPlace
|
||||
? file.FilePath
|
||||
: Path.Combine(_outputFolder, file.FileName);
|
||||
|
||||
var videoIds = file.Tracks
|
||||
.Where(t => t.Type == "video" && t.Copy)
|
||||
.Select(t => t.Id).ToList();
|
||||
var audioIds = file.Tracks
|
||||
.Where(t => t.Type == "audio" && t.Copy)
|
||||
.Select(t => t.Id).ToList();
|
||||
var subIds = file.Tracks
|
||||
.Where(t => t.Type == "subtitles" && t.Copy)
|
||||
.Select(t => t.Id).ToList();
|
||||
|
||||
bool nothingToStrip = file.Tracks.All(t => t.Copy);
|
||||
|
||||
var progress = new Progress<ProcessingProgress>(p =>
|
||||
{
|
||||
job.Percent = p.PercentComplete;
|
||||
job.MegabytesPerSecond = p.MegabytesPerSecond;
|
||||
});
|
||||
|
||||
var result = nothingToStrip
|
||||
? await service.CopyFileAsync(file.FilePath, outputFilePath, progress, ct)
|
||||
: await service.StripTracksAsync(
|
||||
file.FilePath, outputFilePath,
|
||||
videoIds, audioIds, subIds,
|
||||
progress, ct);
|
||||
|
||||
string tag = result.Success ? "[DONE]" : "[FAIL]";
|
||||
AppendLog($"{tag} {result.FileName} — {result.Message}");
|
||||
|
||||
HistoryService.Add(new HistoryEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
FileName = result.FileName,
|
||||
FilePath = file.FilePath,
|
||||
Success = result.Success,
|
||||
Message = result.Message,
|
||||
});
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
successCount++;
|
||||
try
|
||||
{
|
||||
long bytes = new FileInfo(outputFilePath).Length;
|
||||
Interlocked.Add(ref totalBytesWritten, bytes);
|
||||
}
|
||||
catch { /* best effort */ }
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_activeJobs.Remove(job);
|
||||
UpdateTotalSpeed();
|
||||
semaphore.Release();
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
await Task.WhenAll(stripTasks);
|
||||
|
||||
AppendLog(new string('-', 80));
|
||||
AppendLog($"Processing complete: {successCount}/{_files.Count} file(s) succeeded.");
|
||||
|
||||
stopwatch.Stop();
|
||||
double totalSec = stopwatch.Elapsed.TotalSeconds;
|
||||
double totalMb = totalBytesWritten / 1024.0 / 1024.0;
|
||||
double avgMbps = totalSec > 0 ? totalMb / totalSec : 0;
|
||||
AppendLog($"Total data written: {totalMb / 1024.0:F2} GB in {totalSec:F1}s (avg {avgMbps:F1} MB/s combined).");
|
||||
|
||||
StatusText.Text = $"Done: {successCount}/{_files.Count} succeeded.";
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
AppendLog(new string('-', 80));
|
||||
AppendLog("Cancelled by user. Nothing further was modified.");
|
||||
StatusText.Text = "Cancelled.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendLog($"Unexpected error: {ex.Message}");
|
||||
StatusText.Text = "Error.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_activeJobs.Clear();
|
||||
TotalSpeedText.Text = string.Empty;
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
_isDone = true;
|
||||
ProcessingCompleted = true;
|
||||
CancelCloseButton.IsEnabled = true;
|
||||
CancelCloseButton.Content = "Close";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user