forked from sim0n00ps/OF-DL
Add auth flow status updates to the GUI
This commit is contained in:
parent
603c998ae9
commit
3b8e575a21
@ -73,19 +73,33 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
|
||||
/// Launches a browser session and extracts auth data after login.
|
||||
/// </summary>
|
||||
/// <returns>True when auth data is captured successfully.</returns>
|
||||
public async Task<bool> LoadFromBrowserAsync()
|
||||
public async Task<bool> LoadFromBrowserAsync(Action<string>? 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<string> GetBcToken(IPage page) =>
|
||||
await page.EvaluateAsync<string>("window.localStorage.getItem('bcTokenSha') || ''");
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
using FFmpeg.NET;
|
||||
|
||||
@ -18,7 +18,10 @@ public interface IAuthService
|
||||
/// <summary>
|
||||
/// Launches a browser session and extracts auth data after login.
|
||||
/// </summary>
|
||||
Task<bool> LoadFromBrowserAsync();
|
||||
/// <param name="statusCallback">
|
||||
/// Optional callback for reporting status messages to be displayed in the UI.
|
||||
/// </param>
|
||||
Task<bool> LoadFromBrowserAsync(Action<string>? statusCallback = null);
|
||||
|
||||
/// <summary>
|
||||
/// Persists the current auth data to disk.
|
||||
|
||||
@ -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<string, string> validationErrors = ConfigValidationService.Validate(configService.CurrentConfig);
|
||||
IReadOnlyDictionary<string, string> validationErrors =
|
||||
ConfigValidationService.Validate(configService.CurrentConfig);
|
||||
bool hasToolPathErrors = false;
|
||||
FfmpegPathError = string.Empty;
|
||||
FfprobePathError = string.Empty;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -61,10 +61,6 @@ public partial class FaqWindow : Window
|
||||
|
||||
public ObservableCollection<FaqEntry> Entries { get; } = [];
|
||||
|
||||
public string RuntimeSummary { get; private set; } = string.Empty;
|
||||
|
||||
private void OnCloseClick(object? sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void BuildEntries()
|
||||
{
|
||||
Entries.Clear();
|
||||
|
||||
@ -994,6 +994,7 @@
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center">
|
||||
<Button Content="Login with Browser"
|
||||
Classes="primary"
|
||||
IsEnabled="{Binding !IsBrowserLoginInProgress}"
|
||||
Command="{Binding StartBrowserLoginCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
@ -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]!;
|
||||
|
||||
@ -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<Task<bool>>(resultObject!);
|
||||
bool result = await Assert.IsType<Task<bool>>(resultObject);
|
||||
Assert.True(result);
|
||||
Assert.True(File.Exists(tempFilename));
|
||||
Assert.NotNull(dbService.LastUpdateMedia);
|
||||
|
||||
@ -145,36 +145,45 @@ internal sealed class StaticApiService : IApiService
|
||||
|
||||
public Task<List<string>?> GetListUsers(string endpoint) => throw new NotImplementedException();
|
||||
|
||||
public Task<OF_DL.Models.Entities.Purchased.PaidPostCollection> GetPaidPosts(string endpoint, string folder,
|
||||
string username, List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
|
||||
public Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder,
|
||||
string username, List<long> paidPostIds, IStatusReporter statusReporter,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<OF_DL.Models.Entities.Posts.PostCollection> GetPosts(string endpoint, string folder,
|
||||
List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
public Task<OF_DL.Models.Entities.Posts.SinglePostCollection> GetPost(string endpoint, string folder, CancellationToken cancellationToken = default) =>
|
||||
public Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder,
|
||||
List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<OF_DL.Models.Entities.Streams.StreamsCollection> GetStreams(string endpoint, string folder,
|
||||
List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
public Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<OF_DL.Models.Entities.Archived.ArchivedCollection> GetArchived(string endpoint, string folder,
|
||||
IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
public Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder,
|
||||
List<long> paidPostIds, IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<OF_DL.Models.Entities.Messages.MessageCollection> GetMessages(string endpoint, string folder,
|
||||
IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
public Task<ArchivedEntities.ArchivedCollection> GetArchived(string endpoint, string folder,
|
||||
IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<OF_DL.Models.Entities.Purchased.PaidMessageCollection> GetPaidMessages(string endpoint,
|
||||
string folder, string username, IStatusReporter statusReporter, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
public Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder,
|
||||
IStatusReporter statusReporter, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<OF_DL.Models.Entities.Purchased.SinglePaidMessageCollection> GetPaidMessage(string endpoint,
|
||||
public Task<PurchasedEntities.PaidMessageCollection> GetPaidMessages(string endpoint,
|
||||
string folder, string username, IStatusReporter statusReporter,
|
||||
CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
public Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint,
|
||||
string folder, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
public Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users, CancellationToken cancellationToken = default) =>
|
||||
public Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<List<OF_DL.Models.Entities.Purchased.PurchasedTabCollection>> GetPurchasedTab(string endpoint,
|
||||
string folder, Dictionary<string, long> users, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
public Task<List<PurchasedEntities.PurchasedTabCollection>> GetPurchasedTab(string endpoint,
|
||||
string folder, Dictionary<string, long> users, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<UserEntities.User?> 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<Dictionary<long, string>?>(null);
|
||||
|
||||
public Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder, CancellationToken cancellationToken = default) =>
|
||||
public Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
PostHandler?.Invoke(endpoint, folder) ?? Task.FromResult(new PostEntities.SinglePostCollection());
|
||||
|
||||
public Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder, CancellationToken cancellationToken = default) =>
|
||||
public Task<PurchasedEntities.SinglePaidMessageCollection> 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<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users, CancellationToken cancellationToken = default) =>
|
||||
public Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<List<PurchasedEntities.PurchasedTabCollection>> 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<string> _statuses;
|
||||
|
||||
public RecordingStatusReporter(string initialStatus)
|
||||
{
|
||||
_statuses = [initialStatus];
|
||||
}
|
||||
private readonly List<string> _statuses = [initialStatus];
|
||||
|
||||
public IReadOnlyList<string> Statuses => _statuses;
|
||||
|
||||
@ -489,7 +497,8 @@ internal sealed class FakeAuthService : IAuthService
|
||||
|
||||
public Task<bool> LoadFromFileAsync(string filePath = "auth.json") => throw new NotImplementedException();
|
||||
|
||||
public Task<bool> LoadFromBrowserAsync() => throw new NotImplementedException();
|
||||
public Task<bool> LoadFromBrowserAsync(Action<string>? dependencyStatusCallback = null) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task SaveToFileAsync(string filePath = "auth.json") => throw new NotImplementedException();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user