feat: finish implementation of ffmpeg installation on linux

This requires testing on Windows before we can call it a full release.
This commit is contained in:
alphafetus committed 2025-10-03 22:36:31 -04:00
1 parent 4c9e22faf8
commit 3c7cfd23ac
10 files changed
+181 -33

No files matched your search

@@ -0,0 +1,10 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using PantherByte.ViewModels;
namespace PantherByte.Messages;
public class OpenNotificationDialogMessage(string title, string message)
: AsyncRequestMessage<NotificationDialogViewModel?> {
public readonly string Title = title;
public readonly string Message = message;
}
@@ -4,6 +4,6 @@ using PantherByte.ViewModels;
namespace PantherByte.Messages; namespace PantherByte.Messages;
public class OpenProgressDialogMessage(ProcessStartInfo info) : AsyncRequestMessage<ProgressWindowViewModel?> { public class OpenProgressDialogMessage(ProgressWindowViewModel vm) : AsyncRequestMessage<ProgressWindowViewModel?> {
public readonly ProcessStartInfo Info = info; public readonly ProgressWindowViewModel ViewModel = vm;
}; };
+85 -25
View File
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
@@ -101,16 +102,20 @@ public partial class MainWindowViewModel : ViewModelBase {
if (Directory.Exists(tempPath)) { if (Directory.Exists(tempPath)) {
SaveLocation = tempPath; SaveLocation = tempPath;
Console.WriteLine("Successfully set the file path."); Console.WriteLine("Successfully set the file path.");
} else { }
else {
throw new DirectoryNotFoundException($"Directory does not exist: {tempPath}"); throw new DirectoryNotFoundException($"Directory does not exist: {tempPath}");
} }
} else { }
else {
throw new NullReferenceException("The resulting storage folder was null."); throw new NullReferenceException("The resulting storage folder was null.");
} }
} else { }
else {
throw new NullReferenceException("Missing a FileSaveService instance"); throw new NullReferenceException("Missing a FileSaveService instance");
} }
} catch (Exception ex) { }
catch (Exception ex) {
// Exceptions are caught locally for now, but will eventually require a modal error window. // Exceptions are caught locally for now, but will eventually require a modal error window.
Console.WriteLine(ex.ToString()); Console.WriteLine(ex.ToString());
} }
@@ -119,7 +124,6 @@ public partial class MainWindowViewModel : ViewModelBase {
/// <summary> /// <summary>
/// Generates the arguments used for the main command string. /// Generates the arguments used for the main command string.
/// </summary> /// </summary>
[RelayCommand]
private async Task<List<string>> GenerateCmdArgsAsync() { private async Task<List<string>> GenerateCmdArgsAsync() {
// The command string format to use: yt-dlp <link> -t <mp3|mp4|wav> -P <path> --no-playlist // The command string format to use: yt-dlp <link> -t <mp3|mp4|wav> -P <path> --no-playlist
// yt-dlp requires usage of ffmpeg as well. // yt-dlp requires usage of ffmpeg as well.
@@ -130,7 +134,8 @@ public partial class MainWindowViewModel : ViewModelBase {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
Console.WriteLine("Using Windows program."); Console.WriteLine("Using Windows program.");
fileName = "Programs/yt-dlp-win64.exe"; fileName = "Programs/yt-dlp-win64.exe";
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
Console.WriteLine("Using Linux program. May require additional run permissions."); Console.WriteLine("Using Linux program. May require additional run permissions.");
fileName = "Programs/yt-dlp-linux"; fileName = "Programs/yt-dlp-linux";
} }
@@ -140,7 +145,8 @@ public partial class MainWindowViewModel : ViewModelBase {
// WAV files require special command arguments // WAV files require special command arguments
if (Format.Equals("wav")) { if (Format.Equals("wav")) {
args.AddRange(["--extract-audio", "--audio-format", Format]); args.AddRange(["--extract-audio", "--audio-format", Format]);
} else { }
else {
args.AddRange(["-t", Format]); args.AddRange(["-t", Format]);
} }
@@ -148,6 +154,10 @@ public partial class MainWindowViewModel : ViewModelBase {
}); });
} }
/// <summary>
/// Checks if ffmpeg is installed.
/// </summary>
/// <returns>True if ffmpeg is installed, or false if not.</returns>
private async Task<bool> CheckFfmpegInstalled() { private async Task<bool> CheckFfmpegInstalled() {
return await Task.Run(async () => { return await Task.Run(async () => {
ProcessStartInfo ffmpegInfo = new("ffmpeg") { ProcessStartInfo ffmpegInfo = new("ffmpeg") {
@@ -160,13 +170,13 @@ public partial class MainWindowViewModel : ViewModelBase {
StartInfo = ffmpegInfo StartInfo = ffmpegInfo
}; };
bool result = true; var result = true;
try { try {
process.Start(); process.Start();
await process.WaitForExitAsync(); await process.WaitForExitAsync();
} }
catch (Exception ex) { catch (Exception) {
result = false; result = false;
} }
@@ -175,7 +185,9 @@ public partial class MainWindowViewModel : ViewModelBase {
} }
/// <summary> /// <summary>
/// Handles the download process of the file. Generates the command and runs it. /// Handles the download process of the file.
///
/// This command is also where the dependency check/installation is run from.
/// </summary> /// </summary>
[RelayCommand] [RelayCommand]
private async Task DownloadFileAsync() { private async Task DownloadFileAsync() {
@@ -184,40 +196,88 @@ public partial class MainWindowViewModel : ViewModelBase {
var args = await GenerateCmdArgsAsync(); var args = await GenerateCmdArgsAsync();
// Run the command. // Run the command.
try { try {
// ffmpeg installation check
var installed = await CheckFfmpegInstalled();
Console.WriteLine($"Is ffmpeg installed: {installed}");
if (!installed) {
SimpleDialogViewModel vm = new("Dependency ffmpeg was not found. Would you like to install it?",
"Dependency Install");
var result = await WeakReferenceMessenger.Default.Send(new OpenSimpleDialogMessage(vm));
if (result is SimpleDialogViewModel.SimpleDialogResult.Yes) {
// Install the program
installed = await InstallFfmpegAsync();
}
}
// This means installation has failed or the user canceled it.
if (!installed) return;
// The command that's run must work on at least Linux and Windows // The command that's run must work on at least Linux and Windows
ProcessStartInfo info = new() { ProcessStartInfo info = new() {
FileName = args[0], FileName = args[0],
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true,
}; };
info.ArgumentList.AddRange(args[1..]); info.ArgumentList.AddRange(args[1..]);
// Check that ffmpeg is installed.
var temp = await CheckFfmpegInstalled();
// if (!await CheckFfmpegInstalled()) {
SimpleDialogViewModel vm = new("Dependency ffmpeg was not found. Would you like to install it?",
"Dependency Install");
var result = await WeakReferenceMessenger.Default.Send(new OpenSimpleDialogMessage(vm));
if (result is not null) {
Console.WriteLine($"Received result: {result}");
}
// }
// This window isn't linked to the download process yet, but that is the plan. // This window isn't linked to the download process yet, but that is the plan.
await OpenProgressWindowAsync(info); await OpenProgressWindowAsync(info);
} catch (Exception ex) { }
catch (Exception ex) {
Console.WriteLine(ex.ToString()); Console.WriteLine(ex.ToString());
} }
} }
/// <summary>
/// Begins the installation process for ffmpeg and displays a window.
/// </summary>
/// <returns>True if the installation was successful, or false if not.</returns>
private async Task<bool> InstallFfmpegAsync() {
ProcessStartInfo info = new() {
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
var result = false;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
// Run: winget install "FFmpeg (Essentials Build)"
info.Verb = "runas";
info.FileName = "winget";
info.Arguments = "install \"FFmpeg (Essentials Build)\"";
ProgressWindowViewModel vm = new(info) {
StatusMessage = "Installing ffmpeg..."
};
await WeakReferenceMessenger.Default.Send(new OpenProgressDialogMessage(vm));
// If there was an error, return false. Otherwise, true.
result = !vm.Error;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
await WeakReferenceMessenger.Default.Send(new OpenNotificationDialogMessage(
"Linux Install Error",
"Linux users must install ffmpeg manually.\nThis may be developed further in the future."
));
// Since this requires a manual install (we can't run sudo nicely) we'll just return false,
// as we didn't actually install anything.
result = false;
}
return result;
}
/// <summary> /// <summary>
/// Opens a ProgressWindow as a modal dialog. I haven't decided if this window should run the command or if it /// Opens a ProgressWindow as a modal dialog. I haven't decided if this window should run the command or if it
/// should simply display the progress of a command that's running here. /// should simply display the progress of a command that's running here.
/// </summary> /// </summary>
[RelayCommand]
private async Task OpenProgressWindowAsync(ProcessStartInfo info) { private async Task OpenProgressWindowAsync(ProcessStartInfo info) {
await WeakReferenceMessenger.Default.Send(new OpenProgressDialogMessage(info)); ProgressWindowViewModel vm = new(info);
await WeakReferenceMessenger.Default.Send(new OpenProgressDialogMessage(vm));
} }
} }
@@ -0,0 +1,12 @@
namespace PantherByte.ViewModels;
public class NotificationDialogViewModel : ViewModelBase {
public string Title { get; set; } = "Notification";
public string Message { get; set; } = "Testing message";
public NotificationDialogViewModel() { }
public NotificationDialogViewModel(string title, string message) {
(Title, Message) = (title, message);
}
}
@@ -77,7 +77,7 @@ public partial class ProgressWindowViewModel : ViewModelBase, IProgressWindowVie
catch (Exception e) { catch (Exception e) {
Console.WriteLine(e.StackTrace); Console.WriteLine(e.StackTrace);
ProcessStdOut += "Error: " + e.StackTrace + "\n"; ProcessStdOut += "Error: " + e.StackTrace + "\n";
StatusMessage = "Error while downloading video."; StatusMessage = "Error while performing download.";
BarColor = "Red"; BarColor = "Red";
Error = true; Error = true;
} }
+13 -3
View File
@@ -1,4 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using PantherByte.Messages; using PantherByte.Messages;
using PantherByte.ViewModels; using PantherByte.ViewModels;
@@ -12,17 +13,26 @@ public partial class MainWindow : Window {
WeakReferenceMessenger.Default.Register<MainWindow, OpenProgressDialogMessage>(this, WeakReferenceMessenger.Default.Register<MainWindow, OpenProgressDialogMessage>(this,
static (w, m) => { static (w, m) => {
var dialog = new ProgressWindow { var dialog = new ProgressWindow {
DataContext = new ProgressWindowViewModel(m.Info) DataContext = m.ViewModel
}; };
m.Reply(dialog.ShowDialog<ProgressWindowViewModel?>(w)); m.Reply(dialog.ShowDialog<ProgressWindowViewModel?>(w));
}); });
WeakReferenceMessenger.Default.Register<MainWindow, OpenSimpleDialogMessage>(this, WeakReferenceMessenger.Default.Register<MainWindow, OpenSimpleDialogMessage>(this,
static (w, m) => { static async (w, m) => {
var dialog = new SimpleDialog { var dialog = new SimpleDialog {
DataContext = m.ViewModel DataContext = m.ViewModel
}; };
m.Reply(dialog.ShowDialog<SimpleDialogViewModel.SimpleDialogResult?>(w)); var response = dialog.ShowDialog<SimpleDialogViewModel.SimpleDialogResult?>(w);
m.Reply(response);
});
WeakReferenceMessenger.Default.Register<MainWindow, OpenNotificationDialogMessage>(this,
static (w, m) => {
var dialog = new NotificationDialog {
DataContext = new NotificationDialogViewModel(m.Title, m.Message)
};
m.Reply(dialog.ShowDialog<NotificationDialogViewModel?>(w));
}); });
} }
} }
@@ -0,0 +1,28 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:PantherByte.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PantherByte.Views.NotificationDialog"
x:DataType="vm:NotificationDialogViewModel"
Title="{Binding Title}"
WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"
Background="Gray"
CanMaximize="False">
<Design.DataContext>
<vm:NotificationDialogViewModel />
</Design.DataContext>
<StackPanel Spacing="20" Margin="10" HorizontalAlignment="Stretch">
<TextBlock
HorizontalAlignment="Center"
Text="{Binding Message}" />
<Button HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
Content="OK"
Padding="30 10"
Click="OkButton_Click" />
</StackPanel>
</Window>
@@ -0,0 +1,15 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace PantherByte.Views;
public partial class NotificationDialog : Window {
public NotificationDialog() {
InitializeComponent();
}
private void OkButton_Click(object? sender, RoutedEventArgs e) =>
Close(DataContext);
}
+2 -1
View File
@@ -30,7 +30,8 @@
<Expander VerticalAlignment="Top" HorizontalAlignment="Stretch" <Expander VerticalAlignment="Top" HorizontalAlignment="Stretch"
Margin="5" Margin="5"
BorderThickness="0" BorderThickness="0"
Background="Transparent"> Background="Transparent"
x:Name="InfoExpander">
<Expander.Header>More information...</Expander.Header> <Expander.Header>More information...</Expander.Header>
<Grid RowDefinitions="*" ColumnDefinitions="*"> <Grid RowDefinitions="*" ColumnDefinitions="*">
<ScrollViewer Grid.Row="0" <ScrollViewer Grid.Row="0"
+13 -1
View File
@@ -1,3 +1,4 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using PantherByte.ViewModels; using PantherByte.ViewModels;
@@ -7,12 +8,23 @@ public partial class ProgressWindow : Window {
public ProgressWindow() { public ProgressWindow() {
InitializeComponent(); InitializeComponent();
// This feels a little fragile, but we'll leave it for now. // Runs the specified process in the view model when the window is opened.
Opened += async (_, _) => { Opened += async (_, _) => {
if (DataContext is IProgressWindowViewModel vm) if (DataContext is IProgressWindowViewModel vm)
await vm.RunProcessAsync(); await vm.RunProcessAsync();
}; };
// Automatically scrolls to the end of the logs when more information is added.
StdOutText.SizeChanged += (_, _) => ScrollView.ScrollToEnd(); StdOutText.SizeChanged += (_, _) => ScrollView.ScrollToEnd();
// Automatically resizes the window when the user collapses the expander.
InfoExpander.Collapsing += (_, _) => {
if (Content is not Control content) return;
content.Measure(Size.Infinity);
var desired = content.DesiredSize;
Width = desired.Width;
Height = desired.Height;
};
} }
} }