diff --git a/OF DL.Core/Services/AuthService.cs b/OF DL.Core/Services/AuthService.cs
index a277a34..5851fe1 100644
--- a/OF DL.Core/Services/AuthService.cs
+++ b/OF DL.Core/Services/AuthService.cs
@@ -73,19 +73,33 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
/// Launches a browser session and extracts auth data after login.
///
/// True when auth data is captured successfully.
- public async Task LoadFromBrowserAsync()
+ public async Task LoadFromBrowserAsync(Action? statusCallback = null)
{
+ statusCallback?.Invoke("Preparing browser dependencies ...");
+
try
{
bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null;
-
await SetupBrowser(runningInDocker);
+ }
+ catch (Exception ex)
+ {
+ statusCallback?.Invoke("Failed to prepare browser dependencies.");
+ Log.Error(ex, "Failed to download browser dependencies");
+ return false;
+ }
+
+ statusCallback?.Invoke("Please login using the opened Chromium window.");
+
+ try
+ {
CurrentAuth = await GetAuthFromBrowser();
return CurrentAuth != null;
}
catch (Exception ex)
{
+ statusCallback?.Invoke("Failed to get auth from browser.");
Log.Error(ex, "Failed to load auth from browser");
return false;
}
@@ -107,7 +121,7 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
{
string json = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented);
await File.WriteAllTextAsync(filePath, json);
- Log.Debug($"Auth saved to file: {filePath}");
+ Log.Debug("Auth saved to file: {FilePath}", filePath);
}
catch (Exception ex)
{
@@ -115,7 +129,7 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
}
}
- private Task SetupBrowser(bool runningInDocker)
+ private Task SetupBrowser(bool runningInDocker) => Task.Run(() =>
{
if (runningInDocker)
{
@@ -134,15 +148,16 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
if (folders.Any())
{
Log.Information("chromium already downloaded. Skipping install step.");
- return Task.CompletedTask;
+ return;
}
}
int exitCode = Program.Main(["install", "--with-deps", "chromium"]);
- return exitCode != 0
- ? throw new Exception($"Playwright chromium download failed. Exited with code {exitCode}")
- : Task.CompletedTask;
- }
+ if (exitCode != 0)
+ {
+ throw new Exception($"Playwright chromium download failed. Exited with code {exitCode}");
+ }
+ });
private static async Task GetBcToken(IPage page) =>
await page.EvaluateAsync("window.localStorage.getItem('bcTokenSha') || ''");
diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs
index 173d376..c47f6b1 100644
--- a/OF DL.Core/Services/DownloadService.cs
+++ b/OF DL.Core/Services/DownloadService.cs
@@ -1,6 +1,5 @@
using System.Diagnostics;
using System.Globalization;
-using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using FFmpeg.NET;
diff --git a/OF DL.Core/Services/IAuthService.cs b/OF DL.Core/Services/IAuthService.cs
index 5078615..d882872 100644
--- a/OF DL.Core/Services/IAuthService.cs
+++ b/OF DL.Core/Services/IAuthService.cs
@@ -18,7 +18,10 @@ public interface IAuthService
///
/// Launches a browser session and extracts auth data after login.
///
- Task LoadFromBrowserAsync();
+ ///
+ /// Optional callback for reporting status messages to be displayed in the UI.
+ ///
+ Task LoadFromBrowserAsync(Action? statusCallback = null);
///
/// Persists the current auth data to disk.
diff --git a/OF DL.Gui/ViewModels/MainWindowViewModel.cs b/OF DL.Gui/ViewModels/MainWindowViewModel.cs
index f879f55..f702c79 100644
--- a/OF DL.Gui/ViewModels/MainWindowViewModel.cs
+++ b/OF DL.Gui/ViewModels/MainWindowViewModel.cs
@@ -258,6 +258,9 @@ public partial class MainWindowViewModel(
[ObservableProperty] private string _authScreenMessage = string.Empty;
+ [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(StartBrowserLoginCommand))]
+ private bool _isBrowserLoginInProgress;
+
[ObservableProperty] private string _errorMessage = string.Empty;
private string _actualFfmpegPath = string.Empty;
@@ -676,7 +679,7 @@ public partial class MainWindowViewModel(
CreatorConfigEditor.AddCreatorCommand.Execute(null);
}
- [RelayCommand]
+ [RelayCommand(CanExecute = nameof(CanStartBrowserLogin))]
private async Task StartBrowserLoginAsync()
{
if (configService.CurrentConfig.DisableBrowserAuth)
@@ -685,10 +688,29 @@ public partial class MainWindowViewModel(
return;
}
- SetLoading("Opening browser for authentication...");
+ IsBrowserLoginInProgress = true;
AppendLog("Starting browser authentication flow.");
- bool success = await authService.LoadFromBrowserAsync();
+ StartBrowserLoginCommand.NotifyCanExecuteChanged();
+ bool success;
+ try
+ {
+ success = await authService.LoadFromBrowserAsync(message =>
+ {
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ return;
+ }
+
+ AuthScreenMessage = message;
+ AppendLog(message);
+ });
+ }
+ finally
+ {
+ IsBrowserLoginInProgress = false;
+ }
+
if (!success || authService.CurrentAuth == null)
{
AuthScreenMessage =
@@ -963,6 +985,11 @@ public partial class MainWindowViewModel(
CurrentScreen != AppScreen.Loading &&
!IsDownloading;
+ private bool CanStartBrowserLogin() =>
+ CurrentScreen == AppScreen.Auth &&
+ !IsDownloading &&
+ !IsBrowserLoginInProgress;
+
partial void OnCurrentScreenChanged(AppScreen value)
{
OnPropertyChanged(nameof(IsLoadingScreen));
@@ -979,6 +1006,7 @@ public partial class MainWindowViewModel(
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
LogoutCommand.NotifyCanExecuteChanged();
EditConfigCommand.NotifyCanExecuteChanged();
+ StartBrowserLoginCommand.NotifyCanExecuteChanged();
OpenSinglePostOrMessageModalCommand.NotifyCanExecuteChanged();
SubmitSinglePostOrMessageCommand.NotifyCanExecuteChanged();
}
@@ -992,6 +1020,7 @@ public partial class MainWindowViewModel(
RefreshUsersCommand.NotifyCanExecuteChanged();
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
LogoutCommand.NotifyCanExecuteChanged();
+ StartBrowserLoginCommand.NotifyCanExecuteChanged();
OpenSinglePostOrMessageModalCommand.NotifyCanExecuteChanged();
SubmitSinglePostOrMessageCommand.NotifyCanExecuteChanged();
}
@@ -1425,7 +1454,8 @@ public partial class MainWindowViewModel(
private bool ValidateConfiguredToolPathsOnStartup()
{
- IReadOnlyDictionary validationErrors = ConfigValidationService.Validate(configService.CurrentConfig);
+ IReadOnlyDictionary validationErrors =
+ ConfigValidationService.Validate(configService.CurrentConfig);
bool hasToolPathErrors = false;
FfmpegPathError = string.Empty;
FfprobePathError = string.Empty;
diff --git a/OF DL.Gui/Views/AboutWindow.axaml.cs b/OF DL.Gui/Views/AboutWindow.axaml.cs
index 3a8b19a..9d4a604 100644
--- a/OF DL.Gui/Views/AboutWindow.axaml.cs
+++ b/OF DL.Gui/Views/AboutWindow.axaml.cs
@@ -46,8 +46,6 @@ public partial class AboutWindow : Window
private async void OnOpenFfprobeLicenseClick(object? sender, RoutedEventArgs e) =>
await OpenExternalUrlAsync(FfprobeLicenseUrl);
- private void OnCloseClick(object? sender, RoutedEventArgs e) => Close();
-
private async Task OpenExternalUrlAsync(string url)
{
try
diff --git a/OF DL.Gui/Views/FaqWindow.axaml.cs b/OF DL.Gui/Views/FaqWindow.axaml.cs
index 966b92d..221a1d4 100644
--- a/OF DL.Gui/Views/FaqWindow.axaml.cs
+++ b/OF DL.Gui/Views/FaqWindow.axaml.cs
@@ -61,10 +61,6 @@ public partial class FaqWindow : Window
public ObservableCollection Entries { get; } = [];
- public string RuntimeSummary { get; private set; } = string.Empty;
-
- private void OnCloseClick(object? sender, RoutedEventArgs e) => Close();
-
private void BuildEntries()
{
Entries.Clear();
diff --git a/OF DL.Gui/Views/MainWindow.axaml b/OF DL.Gui/Views/MainWindow.axaml
index 748b009..1a4e212 100644
--- a/OF DL.Gui/Views/MainWindow.axaml
+++ b/OF DL.Gui/Views/MainWindow.axaml
@@ -994,6 +994,7 @@
diff --git a/OF DL.Tests/Services/ApiServiceTests.cs b/OF DL.Tests/Services/ApiServiceTests.cs
index 7b327ed..81d9dfd 100644
--- a/OF DL.Tests/Services/ApiServiceTests.cs
+++ b/OF DL.Tests/Services/ApiServiceTests.cs
@@ -515,7 +515,7 @@ public class ApiServiceTests
MethodInfo method = typeof(ApiService).GetMethod("TryGetDrmInfo",
BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("TryGetDrmInfo not found.");
- object?[] args = { files, null, null, null, null };
+ object?[] args = [files, null, null, null, null];
bool result = (bool)method.Invoke(null, args)!;
manifestDash = (string)args[1]!;
cloudFrontPolicy = (string)args[2]!;
diff --git a/OF DL.Tests/Services/DownloadServiceTests.cs b/OF DL.Tests/Services/DownloadServiceTests.cs
index 81c10ab..ca4e993 100644
--- a/OF DL.Tests/Services/DownloadServiceTests.cs
+++ b/OF DL.Tests/Services/DownloadServiceTests.cs
@@ -82,9 +82,10 @@ public class DownloadServiceTests
DownloadService service =
CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService);
- (string decryptionKey, DateTime lastModified, double? mpdDurationSeconds)? result = await service.GetDecryptionInfo(
- "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
- true, false);
+ (string decryptionKey, DateTime lastModified, double? mpdDurationSeconds)? result =
+ await service.GetDecryptionInfo(
+ "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
+ true, false);
Assert.NotNull(result);
Assert.Equal("ofdl-key", result.Value.decryptionKey);
@@ -100,9 +101,10 @@ public class DownloadServiceTests
DownloadService service =
CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService);
- (string decryptionKey, DateTime lastModified, double? mpdDurationSeconds)? result = await service.GetDecryptionInfo(
- "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
- false, false);
+ (string decryptionKey, DateTime lastModified, double? mpdDurationSeconds)? result =
+ await service.GetDecryptionInfo(
+ "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
+ false, false);
Assert.NotNull(result);
Assert.Equal("cdm-key", result.Value.decryptionKey);
@@ -124,7 +126,8 @@ public class DownloadServiceTests
await File.WriteAllTextAsync(tempFilename, "abc");
MediaTrackingDbService dbService = new();
- DownloadService service = CreateService(new FakeConfigService(new Config { ShowScrapeSize = false }), dbService);
+ DownloadService service =
+ CreateService(new FakeConfigService(new Config { ShowScrapeSize = false }), dbService);
ProgressRecorder progress = new();
MethodInfo? finalizeMethod = typeof(DownloadService).GetMethod("FinalizeDrmDownload",
@@ -136,7 +139,7 @@ public class DownloadServiceTests
tempFilename, DateTime.UtcNow, folder, path, customFileName, filename, 1L, "Posts", progress
]);
- bool result = await Assert.IsType>(resultObject!);
+ bool result = await Assert.IsType>(resultObject);
Assert.True(result);
Assert.True(File.Exists(tempFilename));
Assert.NotNull(dbService.LastUpdateMedia);
diff --git a/OF DL.Tests/Services/TestDoubles.cs b/OF DL.Tests/Services/TestDoubles.cs
index d69d026..8259a26 100644
--- a/OF DL.Tests/Services/TestDoubles.cs
+++ b/OF DL.Tests/Services/TestDoubles.cs
@@ -145,36 +145,45 @@ internal sealed class StaticApiService : IApiService
public Task?> GetListUsers(string endpoint) => throw new NotImplementedException();
- public Task GetPaidPosts(string endpoint, string folder,
- string username, List paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
+ public Task GetPaidPosts(string endpoint, string folder,
+ string username, List paidPostIds, IStatusReporter statusReporter,
+ CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
- public Task GetPosts(string endpoint, string folder,
- List paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
-
- public Task GetPost(string endpoint, string folder, CancellationToken cancellationToken = default) =>
+ public Task GetPosts(string endpoint, string folder,
+ List paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
- public Task GetStreams(string endpoint, string folder,
- List paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+ public Task GetPost(string endpoint, string folder,
+ CancellationToken cancellationToken = default) =>
+ throw new NotImplementedException();
- public Task GetArchived(string endpoint, string folder,
- IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+ public Task GetStreams(string endpoint, string folder,
+ List paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
+ throw new NotImplementedException();
- public Task GetMessages(string endpoint, string folder,
- IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+ public Task GetArchived(string endpoint, string folder,
+ IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
+ throw new NotImplementedException();
- public Task GetPaidMessages(string endpoint,
- string folder, string username, IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+ public Task GetMessages(string endpoint, string folder,
+ IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
+ throw new NotImplementedException();
- public Task GetPaidMessage(string endpoint,
+ public Task GetPaidMessages(string endpoint,
+ string folder, string username, IStatusReporter statusReporter,
+ CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ public Task GetPaidMessage(string endpoint,
string folder, CancellationToken cancellationToken = default) => throw new NotImplementedException();
- public Task> GetPurchasedTabUsers(string endpoint, Dictionary users, CancellationToken cancellationToken = default) =>
+ public Task> GetPurchasedTabUsers(string endpoint, Dictionary users,
+ CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
- public Task> GetPurchasedTab(string endpoint,
- string folder, Dictionary users, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+ public Task> GetPurchasedTab(string endpoint,
+ string folder, Dictionary users, CancellationToken cancellationToken = default) =>
+ throw new NotImplementedException();
public Task GetUserInfo(string endpoint, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
@@ -222,10 +231,12 @@ internal sealed class ConfigurableApiService : IApiService
MediaHandler?.Invoke(mediaType, endpoint, username, folder) ??
Task.FromResult?>(null);
- public Task GetPost(string endpoint, string folder, CancellationToken cancellationToken = default) =>
+ public Task GetPost(string endpoint, string folder,
+ CancellationToken cancellationToken = default) =>
PostHandler?.Invoke(endpoint, folder) ?? Task.FromResult(new PostEntities.SinglePostCollection());
- public Task GetPaidMessage(string endpoint, string folder, CancellationToken cancellationToken = default) =>
+ public Task GetPaidMessage(string endpoint, string folder,
+ CancellationToken cancellationToken = default) =>
PaidMessageHandler?.Invoke(endpoint, folder) ??
Task.FromResult(new PurchasedEntities.SinglePaidMessageCollection());
@@ -259,7 +270,8 @@ internal sealed class ConfigurableApiService : IApiService
string username, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
- public Task> GetPurchasedTabUsers(string endpoint, Dictionary users, CancellationToken cancellationToken = default) =>
+ public Task> GetPurchasedTabUsers(string endpoint, Dictionary users,
+ CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task> GetPurchasedTab(string endpoint, string folder,
@@ -295,7 +307,8 @@ internal sealed class OrchestrationDownloadServiceStub : IDownloadService
string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter) =>
throw new NotImplementedException();
- public Task<(string decryptionKey, DateTime lastModified, double? mpdDurationSeconds)?> GetDecryptionInfo(string mpdUrl, string policy,
+ public Task<(string decryptionKey, DateTime lastModified, double? mpdDurationSeconds)?> GetDecryptionInfo(
+ string mpdUrl, string policy,
string signature, string kvp, string mediaId, string contentId, string drmType, bool clientIdBlobMissing,
bool devicePrivateKeyMissing) =>
throw new NotImplementedException();
@@ -459,14 +472,9 @@ internal sealed class RecordingDownloadEventHandler : IDownloadEventHandler
public void OnMessage(string message) => Messages.Add(message);
}
-internal sealed class RecordingStatusReporter : IStatusReporter
+internal sealed class RecordingStatusReporter(string initialStatus) : IStatusReporter
{
- private readonly List _statuses;
-
- public RecordingStatusReporter(string initialStatus)
- {
- _statuses = [initialStatus];
- }
+ private readonly List _statuses = [initialStatus];
public IReadOnlyList Statuses => _statuses;
@@ -489,7 +497,8 @@ internal sealed class FakeAuthService : IAuthService
public Task LoadFromFileAsync(string filePath = "auth.json") => throw new NotImplementedException();
- public Task LoadFromBrowserAsync() => throw new NotImplementedException();
+ public Task LoadFromBrowserAsync(Action? dependencyStatusCallback = null) =>
+ throw new NotImplementedException();
public Task SaveToFileAsync(string filePath = "auth.json") => throw new NotImplementedException();