feat: implement full download capability

This needs some cleanup, but is otherwise fully functional.
This commit is contained in:
alphafetus committed 2025-09-18 14:46:20 -04:00
1 parent 9328fc98fe
commit 71ab0fe1f4
13 files changed
+246 -9

No files matched your search

+2 -3
View File
@@ -8,6 +8,7 @@ using System.Linq;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using PantherByte.Services; using PantherByte.Services;
using PantherByte.Services.Interfaces;
using PantherByte.ViewModels; using PantherByte.ViewModels;
using PantherByte.Views; using PantherByte.Views;
@@ -30,9 +31,7 @@ public partial class App : Application {
// Avoid duplicate validations from both Avalonia and the CommunityToolkit. // Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation(); DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow = new MainWindow { desktop.MainWindow = new MainWindow(new MainWindowViewModel());
DataContext = new MainWindowViewModel(),
};
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton<IFileSaveService>(x => new FileService(desktop.MainWindow)); services.AddSingleton<IFileSaveService>(x => new FileService(desktop.MainWindow));
@@ -0,0 +1,9 @@
using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging.Messages;
using PantherByte.ViewModels;
namespace PantherByte.Messages;
public class OpenProgressDialogMessage(ProcessStartInfo info) : AsyncRequestMessage<ProgressWindowViewModel?> {
public readonly ProcessStartInfo Info = info;
};
+10
View File
@@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<Folder Include="Models\" /> <Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" /> <AvaloniaResource Include="Assets\**" />
<Folder Include="Programs\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -28,4 +29,13 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageReference Include="ReactiveUI" Version="21.0.1" /> <PackageReference Include="ReactiveUI" Version="21.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="Programs\yt-dlp-linux">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Programs\yt-dlp-win64.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>
BIN
View File
Binary file not shown.
Binary file not shown.
+1
View File
@@ -1,6 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using PantherByte.Services.Interfaces;
namespace PantherByte.Services; namespace PantherByte.Services;
@@ -2,7 +2,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
namespace PantherByte; namespace PantherByte.Services.Interfaces;
public interface IFileSaveService { public interface IFileSaveService {
public Task<IStorageFolder?> ChooseFolderAsync(); public Task<IStorageFolder?> ChooseFolderAsync();
+47 -3
View File
@@ -1,10 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using DynamicData;
using PantherByte.Messages;
using PantherByte.Services; using PantherByte.Services;
using ReactiveUI; using ReactiveUI;
using PantherByte.Services.Interfaces;
namespace PantherByte.ViewModels; namespace PantherByte.ViewModels;
@@ -12,6 +18,7 @@ public partial class MainWindowViewModel : ViewModelBase {
private string _link = string.Empty; private string _link = string.Empty;
private string _saveLocation = string.Empty; private string _saveLocation = string.Empty;
private string _format = "mp3"; private string _format = "mp3";
private bool _isDownloading = false;
private bool _canDownload = true; private bool _canDownload = true;
/// <summary> /// <summary>
@@ -24,7 +31,7 @@ public partial class MainWindowViewModel : ViewModelBase {
/// <summary> /// <summary>
/// The format to download the video in. Currently supported options are MP3, MP4, and WAV. This will become the /// The format to download the video in. Currently supported options are MP3, MP4, and WAV. This will become the
/// argument passed to the "-t" argument in <see cref="GenerateCmdStringAsync"/>. /// argument passed to the "-t" argument in <see cref="GenerateCmdArgsAsync"/>.
/// </summary> /// </summary>
public string Format { public string Format {
get => _format; get => _format;
@@ -40,6 +47,15 @@ public partial class MainWindowViewModel : ViewModelBase {
set => this.RaiseAndSetIfChanged(ref _saveLocation, value); set => this.RaiseAndSetIfChanged(ref _saveLocation, value);
} }
/// <summary>
/// Indicates if the command is currently running. When the command is running, the application
/// should disable all inputs and/or open a blocking modal window.
/// </summary>
public bool IsDownloading {
get => _isDownloading;
set => this.RaiseAndSetIfChanged(ref _isDownloading, value);
}
/// <summary> /// <summary>
/// Indicates if the file can be downloaded. If not, the download button is disabled. /// Indicates if the file can be downloaded. If not, the download button is disabled.
/// </summary> /// </summary>
@@ -109,7 +125,17 @@ public partial class MainWindowViewModel : ViewModelBase {
// 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.
// Code will be executed when the button is clicked. // Code will be executed when the button is clicked.
return await Task.Run(() => new List<string>([Link, "-t", Format, "-P", SaveLocation, "--no-playlist"]));
var fileName = "";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
Console.WriteLine("Using Windows program.");
fileName = "Programs/yt-dlp-win64.exe";
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
Console.WriteLine("Using Linux program. May require additional run permissions.");
fileName = "Programs/yt-dlp-linux";
}
return await Task.Run(() => new List<string>([fileName, Link, "-t", Format, "-P", SaveLocation, "--no-playlist"]));
} }
/// <summary> /// <summary>
@@ -122,9 +148,27 @@ public partial class MainWindowViewModel : ViewModelBase {
var args = await GenerateCmdArgsAsync(); var args = await GenerateCmdArgsAsync();
// Run the command. // Run the command.
try { try {
System.Diagnostics.Process.Start("yt-dlp", args); // The command that's run must work on at least Linux and Windows
ProcessStartInfo info = new() {
FileName = args[0],
RedirectStandardOutput = true,
RedirectStandardError = true,
};
info.ArgumentList.AddRange(args[1..]);
// This window isn't linked to the download process yet, but that is the plan.
await OpenProgressWindowAsync(info);
} catch (Exception ex) { } catch (Exception ex) {
Console.WriteLine(ex.ToString()); Console.WriteLine(ex.ToString());
} }
} }
/// <summary>
/// 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.
/// </summary>
[RelayCommand]
private async Task OpenProgressWindowAsync(ProcessStartInfo info) {
await WeakReferenceMessenger.Default.Send(new OpenProgressDialogMessage(info));
}
} }
@@ -0,0 +1,92 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using ReactiveUI;
namespace PantherByte.ViewModels;
public class ProgressWindowViewModel : ViewModelBase {
// Pass variables between the MainWindow and this window, specifically what point the application
// is at in terms of downloading. We'll likely do this as a simple percentage of total progress.
private readonly ProcessStartInfo _processStartInfo;
private string _processStdOut = "";
private bool _error = false;
private bool _inProgress = false;
private string _statusMessage = "Downloading video...";
private string _barColor = "CornflowerBlue";
public string ProcessStdOut {
get => _processStdOut;
set => this.RaiseAndSetIfChanged(ref _processStdOut, value);
}
public string StatusMessage {
get => _statusMessage;
set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
public bool Error {
get => _error;
set => this.RaiseAndSetIfChanged(ref _error, value);
}
public bool InProgress {
get => _inProgress;
set => this.RaiseAndSetIfChanged(ref _inProgress, value);
}
public string BarColor {
get => _barColor;
set => this.RaiseAndSetIfChanged(ref _barColor, value);
}
public ProgressWindowViewModel(ProcessStartInfo info) {
_processStartInfo = info;
Console.WriteLine("Created ProgressWindowViewModel instance");
}
public async Task RunProcessAsync() {
Process downloadProcess = new();
downloadProcess.StartInfo = _processStartInfo;
downloadProcess.OutputDataReceived += (_, e) => {
if (!string.IsNullOrWhiteSpace(e.Data)) {
Console.WriteLine(e.Data);
ProcessStdOut += e.Data + "\n";
}
};
downloadProcess.ErrorDataReceived += (_, e) => {
if (string.IsNullOrWhiteSpace(e.Data)) return;
Console.WriteLine(e.Data);
ProcessStdOut += e.Data + "\n";
Error = true;
StatusMessage = "Errors while downloading video.";
BarColor = "Red";
};
Console.WriteLine("Download process is started.");
InProgress = true;
try {
downloadProcess.Start();
downloadProcess.BeginOutputReadLine();
downloadProcess.BeginErrorReadLine();
await downloadProcess.WaitForExitAsync();
}
catch (Exception e) {
Console.WriteLine(e.StackTrace);
ProcessStdOut += "Error: " + e.StackTrace + "\n";
StatusMessage = "Error while downloading video.";
BarColor = "Red";
Error = true;
}
finally {
InProgress = false;
if (!Error) {
BarColor = "Green";
StatusMessage = "Download finished!";
}
}
}
}
+2 -1
View File
@@ -11,7 +11,8 @@
TransparencyLevelHint="AcrylicBlur" TransparencyLevelHint="AcrylicBlur"
Background="Transparent" Background="Transparent"
SizeToContent="WidthAndHeight" SizeToContent="WidthAndHeight"
CanMaximize="False"> CanMaximize="False"
WindowStartupLocation="CenterScreen">
<Design.DataContext> <Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE, <!-- This only sets the DataContext for the previewer in an IDE,
+14 -1
View File
@@ -1,9 +1,22 @@
using Avalonia.Controls; using Avalonia.Controls;
using CommunityToolkit.Mvvm.Messaging;
using PantherByte.Messages;
using PantherByte.ViewModels;
namespace PantherByte.Views; namespace PantherByte.Views;
public partial class MainWindow : Window { public partial class MainWindow : Window {
public MainWindow() { public MainWindow(MainWindowViewModel viewModel) {
InitializeComponent(); InitializeComponent();
DataContext = viewModel;
WeakReferenceMessenger.Default.Register<MainWindow, OpenProgressDialogMessage>(this,
static (w, m) => {
var dialog = new ProgressWindow {
DataContext = new ProgressWindowViewModel(m.Info)
};
m.Reply(dialog.ShowDialog<ProgressWindowViewModel?>(w));
});
} }
} }
+48
View File
@@ -0,0 +1,48 @@
<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="700" d:DesignHeight="450"
x:Class="PantherByte.Views.ProgressWindow"
x:DataType="vm:ProgressWindowViewModel"
Title="Loading..."
TransparencyLevelHint="AcrylicBlur"
SizeToContent="WidthAndHeight"
CanMaximize="False"
WindowStartupLocation="CenterOwner">
<StackPanel
Width="700"
Spacing="10">
<StackPanel Spacing="10" Margin="10">
<TextBlock
HorizontalAlignment="Center"
Text="{Binding StatusMessage}"/>
<ProgressBar
IsIndeterminate="{Binding InProgress}"
Foreground="{Binding BarColor}"
Maximum="100"
Value="100"/>
</StackPanel>
<Expander VerticalAlignment="Top" HorizontalAlignment="Stretch"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="1"
Header="More information..."
Margin="5">
<Grid RowDefinitions="*" ColumnDefinitions="*">
<ScrollViewer Grid.Row="0"
Height="100"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<TextBlock Text="{Binding ProcessStdOut}"
TextWrapping="Wrap"/>
</ScrollViewer>
</Grid>
</Expander>
</StackPanel>
</Window>
+20
View File
@@ -0,0 +1,20 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PantherByte.ViewModels;
namespace PantherByte.Views;
public partial class ProgressWindow : Window {
public ProgressWindow() {
InitializeComponent();
// This feels a little fragile, but we'll leave it for now.
Opened += async (_, _) => {
if (DataContext is not ProgressWindowViewModel vm)
return;
await vm.RunProcessAsync();
};
}
}