Skip to content

Commit b530d64

Browse files
committed
🪄 Optimize backend output handling
- Rewrite line parsing method to be more efficient. - More consistent naming.
1 parent 8f17dc9 commit b530d64

File tree

4 files changed

+113
-95
lines changed

4 files changed

+113
-95
lines changed

‎YoutubeDl.Wpf/Models/BackendInstance.cs‎

Lines changed: 104 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,8 @@ namespace YoutubeDl.Wpf.Models;
1818
public class BackendInstance : ReactiveObject, IEnableLogger
1919
{
2020
private readonly ObservableSettings _settings;
21-
private readonly Process _dlProcess;
2221
private readonly BackendService _backendService;
23-
private readonly string[] outputSeparators =
24-
{
25-
"[download]",
26-
"of",
27-
"at",
28-
"ETA",
29-
"in",
30-
" ",
31-
};
22+
private readonly Process _process;
3223

3324
public List<string> GeneratedDownloadArguments { get; } = new();
3425

@@ -55,27 +46,27 @@ public BackendInstance(ObservableSettings settings, BackendService backendServic
5546
_settings = settings;
5647
_backendService = backendService;
5748

58-
_dlProcess = new();
59-
_dlProcess.StartInfo.CreateNoWindow = true;
60-
_dlProcess.StartInfo.UseShellExecute = false;
61-
_dlProcess.StartInfo.RedirectStandardError = true;
62-
_dlProcess.StartInfo.RedirectStandardOutput = true;
63-
_dlProcess.StartInfo.StandardErrorEncoding = Encoding.UTF8;
64-
_dlProcess.StartInfo.StandardOutputEncoding = Encoding.UTF8;
65-
_dlProcess.EnableRaisingEvents = true;
49+
_process = new();
50+
_process.StartInfo.CreateNoWindow = true;
51+
_process.StartInfo.UseShellExecute = false;
52+
_process.StartInfo.RedirectStandardError = true;
53+
_process.StartInfo.RedirectStandardOutput = true;
54+
_process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
55+
_process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
56+
_process.EnableRaisingEvents = true;
6657
}
6758

68-
private async Task RunDlAsync(CancellationToken cancellationToken = default)
59+
private async Task RunAsync(CancellationToken cancellationToken = default)
6960
{
70-
if (!_dlProcess.Start())
61+
if (!_process.Start())
7162
throw new InvalidOperationException("Method called when the backend process is running.");
7263

7364
SetStatusRunning();
7465

7566
await Task.WhenAll(
76-
ReadAndParseAsync(_dlProcess.StandardError, cancellationToken),
77-
ReadAndParseAsync(_dlProcess.StandardOutput, cancellationToken),
78-
_dlProcess.WaitForExitAsync(cancellationToken));
67+
ReadAndParseLinesAsync(_process.StandardError, cancellationToken),
68+
ReadAndParseLinesAsync(_process.StandardOutput, cancellationToken),
69+
_process.WaitForExitAsync(cancellationToken));
7970

8071
SetStatusStopped();
8172
}
@@ -95,7 +86,7 @@ private void SetStatusStopped()
9586
_backendService.UpdateProgress();
9687
}
9788

98-
private async Task ReadAndParseAsync(StreamReader reader, CancellationToken cancellationToken = default)
89+
private async Task ReadAndParseLinesAsync(StreamReader reader, CancellationToken cancellationToken = default)
9990
{
10091
while (true)
10192
{
@@ -104,34 +95,61 @@ private async Task ReadAndParseAsync(StreamReader reader, CancellationToken canc
10495
return;
10596

10697
this.Log().Info(line);
107-
ParseDlOutput(line);
98+
ParseLine(line);
10899
}
109100
}
110101

111-
private void ParseDlOutput(string output)
102+
private void ParseLine(ReadOnlySpan<char> line)
112103
{
113-
var parsedStringArray = output.Split(outputSeparators, StringSplitOptions.RemoveEmptyEntries);
114-
if (parsedStringArray.Length >= 2) // valid [download] line
104+
// Example lines:
105+
// [download] 0.0% of 36.35MiB at 20.40KiB/s ETA 30:24
106+
// [download] 65.1% of 36.35MiB at 2.81MiB/s ETA 00:04
107+
// [download] 100% of 36.35MiB in 00:10
108+
109+
// Check and strip the download prefix.
110+
const string downloadPrefix = "[download] ";
111+
if (!line.StartsWith(downloadPrefix, StringComparison.Ordinal))
112+
return;
113+
line = line[downloadPrefix.Length..];
114+
115+
// Parse and strip the percentage.
116+
const string percentageSuffix = "% of ";
117+
var percentageEnd = line.IndexOf(percentageSuffix, StringComparison.Ordinal);
118+
if (percentageEnd == -1 || !double.TryParse(line[..percentageEnd], NumberStyles.AllowLeadingWhite | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var percentage))
119+
return;
120+
DownloadProgressPercentage = percentage / 100;
121+
StatusIndeterminate = false;
122+
_backendService.UpdateProgress();
123+
line = line[(percentageEnd + percentageSuffix.Length)..];
124+
125+
// Case 0: Download in progress
126+
const string speedPrefix = " at ";
127+
var sizeEnd = line.IndexOf(speedPrefix, StringComparison.Ordinal);
128+
if (sizeEnd != -1)
115129
{
116-
ReadOnlySpan<char> percentageString = parsedStringArray[0];
117-
if (percentageString.Length >= 2 && percentageString.EndsWith("%")) // actual percentage
118-
{
119-
if (double.TryParse(percentageString[..^1], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var percentageNumber))
120-
{
121-
DownloadProgressPercentage = percentageNumber / 100;
122-
StatusIndeterminate = false;
123-
_backendService.UpdateProgress();
124-
}
125-
}
130+
// Extract and strip file size.
131+
FileSizeString = line[..sizeEnd].ToString();
132+
line = line[(sizeEnd + speedPrefix.Length)..];
133+
134+
// Extract and strip speed.
135+
const string etaPrefix = " ETA ";
136+
var speedEnd = line.IndexOf(etaPrefix, StringComparison.Ordinal);
137+
if (speedEnd == -1)
138+
return;
139+
DownloadSpeedString = line[..speedEnd].TrimStart().ToString();
140+
line = line[(speedEnd + etaPrefix.Length)..];
126141

127-
// save other info
128-
FileSizeString = parsedStringArray[1];
142+
// Extract ETA string.
143+
DownloadETAString = line.ToString();
144+
return;
145+
}
129146

130-
if (parsedStringArray.Length == 4)
131-
{
132-
DownloadSpeedString = parsedStringArray[2];
133-
DownloadETAString = parsedStringArray[3];
134-
}
147+
// Case 1: Download finished
148+
sizeEnd = line.IndexOf(" in ", StringComparison.Ordinal);
149+
if (sizeEnd != -1)
150+
{
151+
// Extract file size.
152+
FileSizeString = line[..sizeEnd].ToString();
135153
}
136154
}
137155

@@ -248,16 +266,16 @@ public void GenerateDownloadArguments()
248266

249267
public async Task StartDownloadAsync(string link, CancellationToken cancellationToken = default)
250268
{
251-
_dlProcess.StartInfo.FileName = _settings.BackendPath;
252-
_dlProcess.StartInfo.ArgumentList.Clear();
253-
_dlProcess.StartInfo.ArgumentList.AddRange(_settings.BackendGlobalArguments.Select(x => x.Argument));
254-
_dlProcess.StartInfo.ArgumentList.AddRange(GeneratedDownloadArguments);
255-
_dlProcess.StartInfo.ArgumentList.AddRange(_settings.BackendDownloadArguments.Select(x => x.Argument));
256-
_dlProcess.StartInfo.ArgumentList.Add(link);
269+
_process.StartInfo.FileName = _settings.BackendPath;
270+
_process.StartInfo.ArgumentList.Clear();
271+
_process.StartInfo.ArgumentList.AddRange(_settings.BackendGlobalArguments.Select(x => x.Argument));
272+
_process.StartInfo.ArgumentList.AddRange(GeneratedDownloadArguments);
273+
_process.StartInfo.ArgumentList.AddRange(_settings.BackendDownloadArguments.Select(x => x.Argument));
274+
_process.StartInfo.ArgumentList.Add(link);
257275

258276
try
259277
{
260-
await RunDlAsync(cancellationToken);
278+
await RunAsync(cancellationToken);
261279
}
262280
catch (Exception ex)
263281
{
@@ -267,37 +285,60 @@ public async Task StartDownloadAsync(string link, CancellationToken cancellation
267285

268286
public async Task ListFormatsAsync(string link, CancellationToken cancellationToken = default)
269287
{
270-
_dlProcess.StartInfo.FileName = _settings.BackendPath;
271-
_dlProcess.StartInfo.ArgumentList.Clear();
272-
_dlProcess.StartInfo.ArgumentList.AddRange(_settings.BackendGlobalArguments.Select(x => x.Argument));
288+
_process.StartInfo.FileName = _settings.BackendPath;
289+
_process.StartInfo.ArgumentList.Clear();
290+
_process.StartInfo.ArgumentList.AddRange(_settings.BackendGlobalArguments.Select(x => x.Argument));
273291
if (!string.IsNullOrEmpty(_settings.Proxy))
274292
{
275-
_dlProcess.StartInfo.ArgumentList.Add("--proxy");
276-
_dlProcess.StartInfo.ArgumentList.Add(_settings.Proxy);
293+
_process.StartInfo.ArgumentList.Add("--proxy");
294+
_process.StartInfo.ArgumentList.Add(_settings.Proxy);
277295
}
278-
_dlProcess.StartInfo.ArgumentList.Add("-F");
279-
_dlProcess.StartInfo.ArgumentList.Add(link);
296+
_process.StartInfo.ArgumentList.Add("-F");
297+
_process.StartInfo.ArgumentList.Add(link);
280298

281299
try
282300
{
283-
await RunDlAsync(cancellationToken);
301+
await RunAsync(cancellationToken);
284302
}
285303
catch (Exception ex)
286304
{
287305
this.Log().Error(ex);
288306
}
289307
}
290308

291-
public async Task AbortDlAsync(CancellationToken cancellationToken = default)
309+
public async Task UpdateAsync(CancellationToken cancellationToken = default)
292310
{
293-
if (CtrlCHelper.AttachConsole((uint)_dlProcess.Id))
311+
_settings.BackendLastUpdateCheck = DateTimeOffset.Now;
312+
313+
_process.StartInfo.FileName = _settings.BackendPath;
314+
_process.StartInfo.ArgumentList.Clear();
315+
if (!string.IsNullOrEmpty(_settings.Proxy))
316+
{
317+
_process.StartInfo.ArgumentList.Add("--proxy");
318+
_process.StartInfo.ArgumentList.Add(_settings.Proxy);
319+
}
320+
_process.StartInfo.ArgumentList.Add("-U");
321+
322+
try
323+
{
324+
await RunAsync(cancellationToken);
325+
}
326+
catch (Exception ex)
327+
{
328+
this.Log().Error(ex);
329+
}
330+
}
331+
332+
public async Task AbortAsync(CancellationToken cancellationToken = default)
333+
{
334+
if (CtrlCHelper.AttachConsole((uint)_process.Id))
294335
{
295336
CtrlCHelper.SetConsoleCtrlHandler(null, true);
296337
try
297338
{
298339
if (CtrlCHelper.GenerateConsoleCtrlEvent(CtrlCHelper.CTRL_C_EVENT, 0))
299340
{
300-
await _dlProcess.WaitForExitAsync(cancellationToken);
341+
await _process.WaitForExitAsync(cancellationToken);
301342
}
302343
}
303344
catch (Exception ex)
@@ -312,27 +353,4 @@ public async Task AbortDlAsync(CancellationToken cancellationToken = default)
312353
}
313354
this.Log().Info("🛑 Aborted.");
314355
}
315-
316-
public async Task UpdateDlAsync(CancellationToken cancellationToken = default)
317-
{
318-
_settings.BackendLastUpdateCheck = DateTimeOffset.Now;
319-
320-
_dlProcess.StartInfo.FileName = _settings.BackendPath;
321-
_dlProcess.StartInfo.ArgumentList.Clear();
322-
if (!string.IsNullOrEmpty(_settings.Proxy))
323-
{
324-
_dlProcess.StartInfo.ArgumentList.Add("--proxy");
325-
_dlProcess.StartInfo.ArgumentList.Add(_settings.Proxy);
326-
}
327-
_dlProcess.StartInfo.ArgumentList.Add("-U");
328-
329-
try
330-
{
331-
await RunDlAsync(cancellationToken);
332-
}
333-
catch (Exception ex)
334-
{
335-
this.Log().Error(ex);
336-
}
337-
}
338356
}

‎YoutubeDl.Wpf/Models/BackendService.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public void UpdateProgress()
5555

5656
public Task UpdateBackendAsync(CancellationToken cancellationToken = default)
5757
{
58-
var tasks = Instances.Select(x => x.UpdateDlAsync(cancellationToken));
58+
var tasks = Instances.Select(x => x.UpdateAsync(cancellationToken));
5959
return Task.WhenAll(tasks);
6060
}
6161
}

‎YoutubeDl.Wpf/ViewModels/HomeViewModel.cs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public class HomeViewModel : ReactiveValidationObject
5656
public ReactiveCommand<Unit, Unit> OpenDownloadFolderCommand { get; }
5757
public ReactiveCommand<string, Unit> StartDownloadCommand { get; }
5858
public ReactiveCommand<string, Unit> ListFormatsCommand { get; }
59-
public ReactiveCommand<Unit, Unit> AbortDlCommand { get; }
59+
public ReactiveCommand<Unit, Unit> AbortCommand { get; }
6060

6161
public ReactiveCommand<Unit, Unit> OpenAddCustomPresetDialogCommand { get; }
6262
public ReactiveCommand<Unit, Unit> OpenEditCustomPresetDialogCommand { get; }
@@ -162,7 +162,7 @@ public HomeViewModel(ObservableSettings settings, BackendService backendService,
162162
x => x.SharedSettings.DownloadPath,
163163
(useCustomPath, path) => useCustomPath && Directory.Exists(path));
164164

165-
var canStartDl = this.WhenAnyValue(
165+
var canRun = this.WhenAnyValue(
166166
x => x.Link,
167167
x => x.SharedSettings.UseCustomPath,
168168
x => x.SharedSettings.DownloadPath,
@@ -174,7 +174,7 @@ public HomeViewModel(ObservableSettings settings, BackendService backendService,
174174
!string.IsNullOrEmpty(dlBinaryPath) &&
175175
!isRunning);
176176

177-
var canAbortDl = this.WhenAnyValue(x => x.BackendInstance.IsRunning);
177+
var canAbort = this.WhenAnyValue(x => x.BackendInstance.IsRunning);
178178

179179
var canEditOrDeletePreset = this.WhenAnyValue(
180180
x => x.SharedSettings.SelectedPreset,
@@ -187,9 +187,9 @@ public HomeViewModel(ObservableSettings settings, BackendService backendService,
187187
ResetCustomFilenameTemplateCommand = ReactiveCommand.Create(ResetCustomFilenameTemplate, canResetCustomFilenameTemplate);
188188
BrowseDownloadFolderCommand = ReactiveCommand.Create(BrowseDownloadFolder, canBrowseDownloadFolder);
189189
OpenDownloadFolderCommand = ReactiveCommand.Create(OpenDownloadFolder, canOpenDownloadFolder);
190-
StartDownloadCommand = ReactiveCommand.CreateFromTask<string>(BackendInstance.StartDownloadAsync, canStartDl);
191-
ListFormatsCommand = ReactiveCommand.CreateFromTask<string>(BackendInstance.ListFormatsAsync, canStartDl);
192-
AbortDlCommand = ReactiveCommand.CreateFromTask(BackendInstance.AbortDlAsync, canAbortDl);
190+
StartDownloadCommand = ReactiveCommand.CreateFromTask<string>(BackendInstance.StartDownloadAsync, canRun);
191+
ListFormatsCommand = ReactiveCommand.CreateFromTask<string>(BackendInstance.ListFormatsAsync, canRun);
192+
AbortCommand = ReactiveCommand.CreateFromTask(BackendInstance.AbortAsync, canAbort);
193193

194194
OpenAddCustomPresetDialogCommand = ReactiveCommand.Create(OpenAddCustomPresetDialog);
195195
OpenEditCustomPresetDialogCommand = ReactiveCommand.Create(OpenEditCustomPresetDialog, canEditOrDeletePreset);
@@ -198,7 +198,7 @@ public HomeViewModel(ObservableSettings settings, BackendService backendService,
198198

199199
if (SharedSettings.BackendAutoUpdate && !string.IsNullOrEmpty(SharedSettings.BackendPath))
200200
{
201-
_ = BackendInstance.UpdateDlAsync();
201+
_ = BackendInstance.UpdateAsync();
202202
}
203203
}
204204

‎YoutubeDl.Wpf/Views/HomeView.xaml.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ public HomeView()
183183
.DisposeWith(disposables);
184184

185185
this.BindCommand(ViewModel,
186-
viewModel => viewModel.AbortDlCommand,
186+
viewModel => viewModel.AbortCommand,
187187
view => view.abortButton)
188188
.DisposeWith(disposables);
189189

0 commit comments

Comments
 (0)