Add FFprobe Path and DRM Video Duration Match Threshold to the config page

This commit is contained in:
whimsical-c4lic0 2026-02-17 01:36:57 -06:00
parent fccee9a520
commit da40f3d0c5
4 changed files with 186 additions and 1 deletions

View File

@ -10,6 +10,7 @@ internal static class ConfigValidationService
ValidatePath(config.DownloadPath, nameof(Config.DownloadPath), errors, requireExistingFile: false); ValidatePath(config.DownloadPath, nameof(Config.DownloadPath), errors, requireExistingFile: false);
ValidatePath(config.FFmpegPath, nameof(Config.FFmpegPath), errors, requireExistingFile: true); 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) if (config.Timeout.HasValue && config.Timeout.Value <= 0 && config.Timeout.Value != -1)
{ {

View File

@ -49,8 +49,12 @@ public partial class MainWindowViewModel(
{ {
[nameof(Config.FFmpegPath)] = [nameof(Config.FFmpegPath)] =
"Path to the FFmpeg executable. If blank, OF-DL will try the app directory and PATH.", "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)] = [nameof(Config.DownloadPath)] =
"Base download folder. If blank, OF-DL uses __user_data__/sites/OnlyFans/{username}.", "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.DownloadVideos)] = "Download video media when enabled.",
[nameof(Config.DownloadImages)] = "Download image media when enabled.", [nameof(Config.DownloadImages)] = "Download image media when enabled.",
[nameof(Config.DownloadAudios)] = "Download audio 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; [ObservableProperty] private string _errorMessage = string.Empty;
private string _actualFfmpegPath = string.Empty; private string _actualFfmpegPath = string.Empty;
private string _actualFfprobePath = string.Empty;
private string _actualDownloadPath = string.Empty; private string _actualDownloadPath = string.Empty;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FfmpegPathDisplay))] [ObservableProperty] [NotifyPropertyChangedFor(nameof(FfmpegPathDisplay))]
@ -172,12 +177,21 @@ public partial class MainWindowViewModel(
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasFfmpegPathError))] [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasFfmpegPathError))]
private string _ffmpegPathError = string.Empty; 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))] [ObservableProperty] [NotifyPropertyChangedFor(nameof(DownloadPathDisplay))]
private string _downloadPath = string.Empty; private string _downloadPath = string.Empty;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasDownloadPathError))] [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasDownloadPathError))]
private string _downloadPathError = string.Empty; private string _downloadPathError = string.Empty;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(DrmVideoDurationMatchThresholdPercentLabel))]
private double _drmVideoDurationMatchThresholdPercent = 98;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasMediaTypesError))] [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasMediaTypesError))]
private string _mediaTypesError = string.Empty; private string _mediaTypesError = string.Empty;
@ -218,6 +232,8 @@ public partial class MainWindowViewModel(
public bool HasFfmpegPathError => !string.IsNullOrWhiteSpace(FfmpegPathError); public bool HasFfmpegPathError => !string.IsNullOrWhiteSpace(FfmpegPathError);
public bool HasFfprobePathError => !string.IsNullOrWhiteSpace(FfprobePathError);
public bool HasDownloadPathError => !string.IsNullOrWhiteSpace(DownloadPathError); public bool HasDownloadPathError => !string.IsNullOrWhiteSpace(DownloadPathError);
public bool HasMediaTypesError => !string.IsNullOrWhiteSpace(MediaTypesError); public bool HasMediaTypesError => !string.IsNullOrWhiteSpace(MediaTypesError);
@ -233,8 +249,16 @@ public partial class MainWindowViewModel(
public string FfmpegPathHelpText => GetConfigHelpText(nameof(Config.FFmpegPath)); public string FfmpegPathHelpText => GetConfigHelpText(nameof(Config.FFmpegPath));
public string FfprobePathHelpText => GetConfigHelpText(nameof(Config.FFprobePath));
public string DownloadPathHelpText => GetConfigHelpText(nameof(Config.DownloadPath)); public string DownloadPathHelpText => GetConfigHelpText(nameof(Config.DownloadPath));
public string DrmVideoDurationMatchThresholdHelpText =>
GetConfigHelpText(nameof(Config.DrmVideoDurationMatchThreshold));
public string DrmVideoDurationMatchThresholdPercentLabel =>
$"{Math.Round(DrmVideoDurationMatchThresholdPercent)}%";
public string SelectedUsersSummary => public string SelectedUsersSummary =>
$"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected"; $"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected";
@ -319,6 +343,16 @@ public partial class MainWindowViewModel(
FfmpegPathError = string.Empty; 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) public void SetDownloadPath(string? path)
{ {
string normalizedPath = NormalizePathForDisplay(path); string normalizedPath = NormalizePathForDisplay(path);
@ -760,6 +794,16 @@ public partial class MainWindowViewModel(
FfmpegPathError = string.Empty; FfmpegPathError = string.Empty;
} }
partial void OnFfprobePathChanged(string value)
{
if (value != "[Hidden for Privacy]")
{
_actualFfprobePath = value;
}
FfprobePathError = string.Empty;
}
partial void OnDownloadPathChanged(string value) partial void OnDownloadPathChanged(string value)
{ {
if (value != "[Hidden for Privacy]") if (value != "[Hidden for Privacy]")
@ -1009,6 +1053,12 @@ public partial class MainWindowViewModel(
continue; continue;
} }
if (error.Key == nameof(Config.FFprobePath))
{
FfprobePathError = error.Value;
continue;
}
if (fieldMap.TryGetValue(error.Key, out ConfigFieldViewModel? field)) if (fieldMap.TryGetValue(error.Key, out ConfigFieldViewModel? field))
{ {
field.SetError(error.Value); field.SetError(error.Value);
@ -1122,12 +1172,18 @@ public partial class MainWindowViewModel(
_actualFfmpegPath = ffmpegPath; _actualFfmpegPath = ffmpegPath;
FfmpegPath = HidePrivateInfo && !string.IsNullOrWhiteSpace(ffmpegPath) ? "[Hidden for Privacy]" : 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); string downloadPath = ResolveDownloadPathForDisplay(config.DownloadPath);
_actualDownloadPath = downloadPath; _actualDownloadPath = downloadPath;
DownloadPath = HidePrivateInfo && !string.IsNullOrWhiteSpace(downloadPath) DownloadPath = HidePrivateInfo && !string.IsNullOrWhiteSpace(downloadPath)
? "[Hidden for Privacy]" ? "[Hidden for Privacy]"
: downloadPath; : downloadPath;
DrmVideoDurationMatchThresholdPercent = Math.Clamp(config.DrmVideoDurationMatchThreshold * 100, 0, 100);
ClearSpecialConfigErrors(); ClearSpecialConfigErrors();
PopulateSelectionOptions(MediaTypeOptions, s_mediaTypeOptions, config); PopulateSelectionOptions(MediaTypeOptions, s_mediaTypeOptions, config);
@ -1168,11 +1224,19 @@ public partial class MainWindowViewModel(
? string.Empty ? string.Empty
: EscapePathForConfig(normalizedFfmpegPath); : 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 downloadPathToUse = HidePrivateInfo ? _actualDownloadPath : DownloadPath;
string normalizedDownloadPath = NormalizePathForDisplay(downloadPathToUse); string normalizedDownloadPath = NormalizePathForDisplay(downloadPathToUse);
config.DownloadPath = string.IsNullOrWhiteSpace(normalizedDownloadPath) config.DownloadPath = string.IsNullOrWhiteSpace(normalizedDownloadPath)
? EscapePathForConfig(s_defaultDownloadPath) ? EscapePathForConfig(s_defaultDownloadPath)
: EscapePathForConfig(normalizedDownloadPath); : EscapePathForConfig(normalizedDownloadPath);
config.DrmVideoDurationMatchThreshold =
Math.Clamp(Math.Round(DrmVideoDurationMatchThresholdPercent / 100d, 2), 0d, 1d);
ApplySelectionOptionsToConfig(config, MediaTypeOptions); ApplySelectionOptionsToConfig(config, MediaTypeOptions);
ApplySelectionOptionsToConfig(config, MediaSourceOptions); ApplySelectionOptionsToConfig(config, MediaSourceOptions);
@ -1208,13 +1272,14 @@ public partial class MainWindowViewModel(
private void ClearSpecialConfigErrors() private void ClearSpecialConfigErrors()
{ {
FfmpegPathError = string.Empty; FfmpegPathError = string.Empty;
FfprobePathError = string.Empty;
DownloadPathError = string.Empty; DownloadPathError = string.Empty;
MediaTypesError = string.Empty; MediaTypesError = string.Empty;
MediaSourcesError = string.Empty; MediaSourcesError = string.Empty;
} }
private bool HasSpecialConfigErrors() => private bool HasSpecialConfigErrors() =>
HasFfmpegPathError || HasDownloadPathError || HasMediaTypesError || HasMediaSourcesError; HasFfmpegPathError || HasFfprobePathError || HasDownloadPathError || HasMediaTypesError || HasMediaSourcesError;
private static string ResolveDownloadPathForDisplay(string? configuredPath) private static string ResolveDownloadPathForDisplay(string? configuredPath)
{ {
@ -1404,7 +1469,9 @@ public partial class MainWindowViewModel(
or nameof(Config.NonInteractiveModePurchasedTab) or nameof(Config.NonInteractiveModePurchasedTab)
or nameof(Config.DisableBrowserAuth) or nameof(Config.DisableBrowserAuth)
or nameof(Config.FFmpegPath) or nameof(Config.FFmpegPath)
or nameof(Config.FFprobePath)
or nameof(Config.DownloadPath) or nameof(Config.DownloadPath)
or nameof(Config.DrmVideoDurationMatchThreshold)
or nameof(Config.DownloadVideos) or nameof(Config.DownloadVideos)
or nameof(Config.DownloadImages) or nameof(Config.DownloadImages)
or nameof(Config.DownloadAudios) or nameof(Config.DownloadAudios)

View File

@ -142,6 +142,42 @@
Foreground="#FF5A5A" Foreground="#FF5A5A"
Text="{Binding FfmpegPathError}" Text="{Binding FfmpegPathError}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,8,0,0">
<TextBlock FontWeight="SemiBold"
Foreground="#1F2A44"
Text="FFprobe Path" />
<Button Width="20"
Height="20"
MinWidth="20"
MinHeight="20"
Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
BorderThickness="1"
CornerRadius="10"
Content="?"
ToolTip.Tip="{Binding FfprobePathHelpText}" />
</StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding FfprobePath}"
Watermark="Select ffprobe executable" />
<Button Grid.Column="1"
Margin="8,0,0,0"
Classes="secondary"
Content="Browse..."
Click="OnBrowseFfprobePathClick" />
</Grid>
<TextBlock IsVisible="{Binding HasFfprobePathError}"
Foreground="#FF5A5A"
Text="{Binding FfprobePathError}"
TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</Border> </Border>
@ -291,6 +327,51 @@
Foreground="#FF5A5A" Foreground="#FF5A5A"
Text="{Binding ViewModel.DownloadPathError, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=''}" Text="{Binding ViewModel.DownloadPathError, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=''}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<Grid Margin="0,8,0,2" ColumnDefinitions="190,*">
<Grid Grid.Column="0"
ColumnDefinitions="*,Auto"
Margin="0,6,10,0"
ClipToBounds="True">
<TextBlock Grid.Column="0"
FontWeight="SemiBold"
Foreground="#1F2A44"
Text="DRM Video Duration Match Threshold"
TextWrapping="Wrap"
VerticalAlignment="Top" />
<Button Grid.Column="1"
Width="20"
Height="20"
MinWidth="20"
MinHeight="20"
Margin="6,0,0,0"
Padding="0"
VerticalAlignment="Top"
HorizontalAlignment="Right"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
BorderThickness="1"
CornerRadius="10"
Content="?"
ToolTip.Tip="{Binding ViewModel.DrmVideoDurationMatchThresholdHelpText, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=''}" />
</Grid>
<Grid Grid.Column="1" ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<Slider Grid.Column="0"
Minimum="0"
Maximum="100"
Value="{Binding ViewModel.DrmVideoDurationMatchThresholdPercent, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=98}" />
<TextBlock Grid.Column="1"
Margin="10,0,0,0"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="#4A5B78"
Text="{Binding ViewModel.DrmVideoDurationMatchThresholdPercentLabel, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue='98%'}" />
</Grid>
</Grid>
</StackPanel> </StackPanel>
<StackPanel IsVisible="{Binding HasSpecificDateFilterFields}" <StackPanel IsVisible="{Binding HasSpecificDateFilterFields}"
Margin="0,0,0,6" Margin="0,0,0,6"

View File

@ -70,6 +70,42 @@ public partial class MainWindow : Window
vm.SetFfmpegPath(selectedFile.Name); vm.SetFfmpegPath(selectedFile.Name);
} }
private async void OnBrowseFfprobePathClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm)
{
return;
}
TopLevel? topLevel = TopLevel.GetTopLevel(this);
if (topLevel?.StorageProvider == null)
{
return;
}
IReadOnlyList<IStorageFile> 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) private async void OnBrowseDownloadPathClick(object? sender, RoutedEventArgs e)
{ {
if (DataContext is not MainWindowViewModel vm) if (DataContext is not MainWindowViewModel vm)