diff --git a/OF DL.Gui/Services/ConfigValidationService.cs b/OF DL.Gui/Services/ConfigValidationService.cs
index bf83c89..2374a3e 100644
--- a/OF DL.Gui/Services/ConfigValidationService.cs
+++ b/OF DL.Gui/Services/ConfigValidationService.cs
@@ -10,6 +10,7 @@ internal static class ConfigValidationService
ValidatePath(config.DownloadPath, nameof(Config.DownloadPath), errors, requireExistingFile: false);
ValidatePath(config.FFmpegPath, nameof(Config.FFmpegPath), errors, requireExistingFile: true);
+ ValidatePath(config.FFprobePath, nameof(Config.FFprobePath), errors, requireExistingFile: true);
if (config.Timeout.HasValue && config.Timeout.Value <= 0 && config.Timeout.Value != -1)
{
diff --git a/OF DL.Gui/ViewModels/MainWindowViewModel.cs b/OF DL.Gui/ViewModels/MainWindowViewModel.cs
index 5ac9b96..150908e 100644
--- a/OF DL.Gui/ViewModels/MainWindowViewModel.cs
+++ b/OF DL.Gui/ViewModels/MainWindowViewModel.cs
@@ -49,8 +49,12 @@ public partial class MainWindowViewModel(
{
[nameof(Config.FFmpegPath)] =
"Path to the FFmpeg executable. If blank, OF-DL will try the app directory and PATH.",
+ [nameof(Config.FFprobePath)] =
+ "Path to the FFprobe executable. If blank, OF-DL will try FFmpeg's directory, the app directory, and PATH.",
[nameof(Config.DownloadPath)] =
"Base download folder. If blank, OF-DL uses __user_data__/sites/OnlyFans/{username}.",
+ [nameof(Config.DrmVideoDurationMatchThreshold)] =
+ "Minimum DRM video duration match threshold. Higher values are stricter. 100% requires an exact duration match. 98% is the recommended value.",
[nameof(Config.DownloadVideos)] = "Download video media when enabled.",
[nameof(Config.DownloadImages)] = "Download image media when enabled.",
[nameof(Config.DownloadAudios)] = "Download audio media when enabled.",
@@ -164,6 +168,7 @@ public partial class MainWindowViewModel(
[ObservableProperty] private string _errorMessage = string.Empty;
private string _actualFfmpegPath = string.Empty;
+ private string _actualFfprobePath = string.Empty;
private string _actualDownloadPath = string.Empty;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FfmpegPathDisplay))]
@@ -172,12 +177,21 @@ public partial class MainWindowViewModel(
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasFfmpegPathError))]
private string _ffmpegPathError = string.Empty;
+ [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasFfprobePathError))]
+ private string _ffprobePath = string.Empty;
+
+ [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasFfprobePathError))]
+ private string _ffprobePathError = string.Empty;
+
[ObservableProperty] [NotifyPropertyChangedFor(nameof(DownloadPathDisplay))]
private string _downloadPath = string.Empty;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasDownloadPathError))]
private string _downloadPathError = string.Empty;
+ [ObservableProperty] [NotifyPropertyChangedFor(nameof(DrmVideoDurationMatchThresholdPercentLabel))]
+ private double _drmVideoDurationMatchThresholdPercent = 98;
+
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasMediaTypesError))]
private string _mediaTypesError = string.Empty;
@@ -218,6 +232,8 @@ public partial class MainWindowViewModel(
public bool HasFfmpegPathError => !string.IsNullOrWhiteSpace(FfmpegPathError);
+ public bool HasFfprobePathError => !string.IsNullOrWhiteSpace(FfprobePathError);
+
public bool HasDownloadPathError => !string.IsNullOrWhiteSpace(DownloadPathError);
public bool HasMediaTypesError => !string.IsNullOrWhiteSpace(MediaTypesError);
@@ -233,8 +249,16 @@ public partial class MainWindowViewModel(
public string FfmpegPathHelpText => GetConfigHelpText(nameof(Config.FFmpegPath));
+ public string FfprobePathHelpText => GetConfigHelpText(nameof(Config.FFprobePath));
+
public string DownloadPathHelpText => GetConfigHelpText(nameof(Config.DownloadPath));
+ public string DrmVideoDurationMatchThresholdHelpText =>
+ GetConfigHelpText(nameof(Config.DrmVideoDurationMatchThreshold));
+
+ public string DrmVideoDurationMatchThresholdPercentLabel =>
+ $"{Math.Round(DrmVideoDurationMatchThresholdPercent)}%";
+
public string SelectedUsersSummary =>
$"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected";
@@ -319,6 +343,16 @@ public partial class MainWindowViewModel(
FfmpegPathError = string.Empty;
}
+ public void SetFfprobePath(string? path)
+ {
+ string normalizedPath = NormalizePathForDisplay(path);
+ _actualFfprobePath = normalizedPath;
+ FfprobePath = HidePrivateInfo && !string.IsNullOrWhiteSpace(normalizedPath)
+ ? "[Hidden for Privacy]"
+ : normalizedPath;
+ FfprobePathError = string.Empty;
+ }
+
public void SetDownloadPath(string? path)
{
string normalizedPath = NormalizePathForDisplay(path);
@@ -760,6 +794,16 @@ public partial class MainWindowViewModel(
FfmpegPathError = string.Empty;
}
+ partial void OnFfprobePathChanged(string value)
+ {
+ if (value != "[Hidden for Privacy]")
+ {
+ _actualFfprobePath = value;
+ }
+
+ FfprobePathError = string.Empty;
+ }
+
partial void OnDownloadPathChanged(string value)
{
if (value != "[Hidden for Privacy]")
@@ -1009,6 +1053,12 @@ public partial class MainWindowViewModel(
continue;
}
+ if (error.Key == nameof(Config.FFprobePath))
+ {
+ FfprobePathError = error.Value;
+ continue;
+ }
+
if (fieldMap.TryGetValue(error.Key, out ConfigFieldViewModel? field))
{
field.SetError(error.Value);
@@ -1122,12 +1172,18 @@ public partial class MainWindowViewModel(
_actualFfmpegPath = ffmpegPath;
FfmpegPath = HidePrivateInfo && !string.IsNullOrWhiteSpace(ffmpegPath) ? "[Hidden for Privacy]" : ffmpegPath;
+ string ffprobePath = NormalizePathForDisplay(config.FFprobePath);
+ _actualFfprobePath = ffprobePath;
+ FfprobePath = HidePrivateInfo && !string.IsNullOrWhiteSpace(ffprobePath) ? "[Hidden for Privacy]" : ffprobePath;
+
string downloadPath = ResolveDownloadPathForDisplay(config.DownloadPath);
_actualDownloadPath = downloadPath;
DownloadPath = HidePrivateInfo && !string.IsNullOrWhiteSpace(downloadPath)
? "[Hidden for Privacy]"
: downloadPath;
+ DrmVideoDurationMatchThresholdPercent = Math.Clamp(config.DrmVideoDurationMatchThreshold * 100, 0, 100);
+
ClearSpecialConfigErrors();
PopulateSelectionOptions(MediaTypeOptions, s_mediaTypeOptions, config);
@@ -1168,11 +1224,19 @@ public partial class MainWindowViewModel(
? string.Empty
: EscapePathForConfig(normalizedFfmpegPath);
+ string ffprobePathToUse = HidePrivateInfo ? _actualFfprobePath : FfprobePath;
+ string normalizedFfprobePath = NormalizePathForDisplay(ffprobePathToUse);
+ config.FFprobePath = string.IsNullOrWhiteSpace(normalizedFfprobePath)
+ ? string.Empty
+ : EscapePathForConfig(normalizedFfprobePath);
+
string downloadPathToUse = HidePrivateInfo ? _actualDownloadPath : DownloadPath;
string normalizedDownloadPath = NormalizePathForDisplay(downloadPathToUse);
config.DownloadPath = string.IsNullOrWhiteSpace(normalizedDownloadPath)
? EscapePathForConfig(s_defaultDownloadPath)
: EscapePathForConfig(normalizedDownloadPath);
+ config.DrmVideoDurationMatchThreshold =
+ Math.Clamp(Math.Round(DrmVideoDurationMatchThresholdPercent / 100d, 2), 0d, 1d);
ApplySelectionOptionsToConfig(config, MediaTypeOptions);
ApplySelectionOptionsToConfig(config, MediaSourceOptions);
@@ -1208,13 +1272,14 @@ public partial class MainWindowViewModel(
private void ClearSpecialConfigErrors()
{
FfmpegPathError = string.Empty;
+ FfprobePathError = string.Empty;
DownloadPathError = string.Empty;
MediaTypesError = string.Empty;
MediaSourcesError = string.Empty;
}
private bool HasSpecialConfigErrors() =>
- HasFfmpegPathError || HasDownloadPathError || HasMediaTypesError || HasMediaSourcesError;
+ HasFfmpegPathError || HasFfprobePathError || HasDownloadPathError || HasMediaTypesError || HasMediaSourcesError;
private static string ResolveDownloadPathForDisplay(string? configuredPath)
{
@@ -1404,7 +1469,9 @@ public partial class MainWindowViewModel(
or nameof(Config.NonInteractiveModePurchasedTab)
or nameof(Config.DisableBrowserAuth)
or nameof(Config.FFmpegPath)
+ or nameof(Config.FFprobePath)
or nameof(Config.DownloadPath)
+ or nameof(Config.DrmVideoDurationMatchThreshold)
or nameof(Config.DownloadVideos)
or nameof(Config.DownloadImages)
or nameof(Config.DownloadAudios)
diff --git a/OF DL.Gui/Views/MainWindow.axaml b/OF DL.Gui/Views/MainWindow.axaml
index 230d1ee..59a7d2f 100644
--- a/OF DL.Gui/Views/MainWindow.axaml
+++ b/OF DL.Gui/Views/MainWindow.axaml
@@ -142,6 +142,42 @@
Foreground="#FF5A5A"
Text="{Binding FfmpegPathError}"
TextWrapping="Wrap" />
+
+
+
+
+
+
+
+
+
+
@@ -291,6 +327,51 @@
Foreground="#FF5A5A"
Text="{Binding ViewModel.DownloadPathError, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=''}"
TextWrapping="Wrap" />
+
+
+
+
+
+
+
+
+
+
+
selectedFiles = await topLevel.StorageProvider.OpenFilePickerAsync(
+ new FilePickerOpenOptions
+ {
+ Title = "Select FFprobe executable",
+ AllowMultiple = false
+ });
+
+ IStorageFile? selectedFile = selectedFiles.FirstOrDefault();
+ if (selectedFile == null)
+ {
+ return;
+ }
+
+ string? localPath = selectedFile.TryGetLocalPath();
+ if (!string.IsNullOrWhiteSpace(localPath))
+ {
+ vm.SetFfprobePath(localPath);
+ return;
+ }
+
+ vm.SetFfprobePath(selectedFile.Name);
+ }
+
private async void OnBrowseDownloadPathClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm)