Compare commits

...

4 commits

Author SHA1 Message Date
Heavy Bob
b448cfec3c Point streamlink test to /test
Streamlink tests now throw at the /test api and expect a streamer variable, if nothing then it will succeed. Think the streamer link stuff is mostly done, just need to do the changes on the api.
2025-04-10 03:16:42 +10:00
Heavy Bob
5994743c73 Added checks for streamlink response for web handler
I've also put in a check to make sure if we are going to accept a response from the api that it it's valid.
2025-04-10 02:46:35 +10:00
Heavy Bob
d7057efe15 Streamlink - Almost done. 2025-04-10 01:55:35 +10:00
Heavy Bob
f7fb63c50f Fixed /register-kill not using filtering.
/register-kill didn't use the same filtering as /test resulting in users failing to get kills because url malformed. /test was also hanging app for response so fixed this.
2025-04-09 23:53:07 +10:00
9 changed files with 607 additions and 173 deletions

View file

@ -571,5 +571,55 @@
</Setter> </Setter>
</Style> </Style>
<!-- Standard Slider Style -->
<Style x:Key="SliderStyle" TargetType="Slider">
<Setter Property="Height" Value="40"/>
<Setter Property="Width" Value="160"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="Background" Value="{DynamicResource BackgroundDarkBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Width="100" Height="30" HorizontalAlignment="Left">
<!-- Track Background -->
<Border Background="{DynamicResource BackgroundDarkBrush}"
BorderBrush="{DynamicResource AccentBrush}"
BorderThickness="2"
CornerRadius="15"
Margin="0,0,-5,-4"/>
<!-- Track -->
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb x:Name="PART_Thumb"
Width="22"
Height="22"
Margin="6,4,1,0">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Ellipse Fill="{DynamicResource AccentBrush}"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
<Track.DecreaseRepeatButton>
<RepeatButton Background="Transparent"
BorderBrush="Transparent"
IsHitTestVisible="False"/>
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Background="Transparent"
BorderBrush="Transparent"
IsHitTestVisible="False"/>
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources> </Application.Resources>
</Application> </Application>

View file

@ -19,6 +19,7 @@ namespace AutoTrackR2
"AutoTrackR2", "AutoTrackR2",
"crash.log" "crash.log"
); );
private StreamlinkHandler? _streamlinkHandler;
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
@ -43,6 +44,7 @@ namespace AutoTrackR2
} }
base.OnStartup(e); base.OnStartup(e);
_streamlinkHandler = new StreamlinkHandler();
} }
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)

View file

@ -2,14 +2,13 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="410" Height="410"
Width="626"> Width="700">
<Grid Background="{DynamicResource BackgroundLightBrush}"> <Grid Background="{DynamicResource BackgroundLightBrush}">
<!-- Main Layout Grid --> <!-- Main Layout Grid -->
<Grid Margin="0,0,5,7"> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<!-- One row for the content, the other for buttons -->
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
@ -22,9 +21,9 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Section for Config Fields --> <!-- Section for Config Fields -->
<StackPanel Grid.Column="0" <StackPanel Grid.Row="0"
VerticalAlignment="Center" VerticalAlignment="Top"
Height="389"> Margin="20,10,20,0">
<!-- Log File --> <!-- Log File -->
<StackPanel Margin="0,5,0,10" <StackPanel Margin="0,5,0,10"
Orientation="Horizontal"> Orientation="Horizontal">
@ -38,11 +37,11 @@
FontSize="16" FontSize="16"
Margin="0,5,0,5" Margin="0,5,0,5"
FontFamily="{StaticResource Roboto}"/> FontFamily="{StaticResource Roboto}"/>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal">
Margin="30,0,0,0">
<TextBox Name="LogFilePath" <TextBox Name="LogFilePath"
Width="330" Width="260"
Height="30" Height="30"
Margin="10,0,0,0"
Style="{StaticResource RoundedTextBox}"/> Style="{StaticResource RoundedTextBox}"/>
<Button Content="Browse" <Button Content="Browse"
Width="75" Width="75"
@ -51,6 +50,13 @@
Margin="5,0" Margin="5,0"
Style="{StaticResource ButtonStyle}" Style="{StaticResource ButtonStyle}"
Click="LogFileBrowseButton_Click"/> Click="LogFileBrowseButton_Click"/>
<Button Content="Open"
Width="75"
Height="30"
FontFamily="{StaticResource Orbitron}"
Margin="5,0"
Style="{StaticResource ButtonStyle}"
Click="LogFileOpenButton_Click"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
@ -66,11 +72,11 @@
Foreground="{DynamicResource TextBrush}" Foreground="{DynamicResource TextBrush}"
FontSize="16" FontSize="16"
Margin="0,5,0,5"/> Margin="0,5,0,5"/>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal">
Margin="30,0,0,0">
<TextBox Name="ApiUrl" <TextBox Name="ApiUrl"
Width="330" Width="260"
Height="30" Height="30"
Margin="10,0,0,0"
Style="{StaticResource RoundedTextBox}"/> Style="{StaticResource RoundedTextBox}"/>
<Button Content="Test" <Button Content="Test"
Width="75" Width="75"
@ -95,9 +101,9 @@
FontSize="16" FontSize="16"
Margin="0,5,0,5"/> Margin="0,5,0,5"/>
<PasswordBox Name="ApiKey" <PasswordBox Name="ApiKey"
Width="330" Width="260"
Height="30" Height="30"
Margin="33,0,0,0" Margin="10,0,0,0"
Style="{StaticResource RoundedPasswordBox}"/> Style="{StaticResource RoundedPasswordBox}"/>
</StackPanel> </StackPanel>
@ -115,7 +121,7 @@
Margin="0,5,0,5"/> Margin="0,5,0,5"/>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBox Name="VideoPath" <TextBox Name="VideoPath"
Width="330" Width="260"
Height="30" Height="30"
Margin="10,0,0,0" Margin="10,0,0,0"
Style="{StaticResource RoundedTextBox}"/> Style="{StaticResource RoundedTextBox}"/>
@ -126,130 +132,214 @@
Margin="5,0" Margin="5,0"
Style="{StaticResource ButtonStyle}" Style="{StaticResource ButtonStyle}"
Click="VideoPathBrowseButton_Click"/> Click="VideoPathBrowseButton_Click"/>
<Button Content="Open"
Width="75"
Height="30"
FontFamily="{StaticResource Orbitron}"
Margin="5,0"
Style="{StaticResource ButtonStyle}"
Click="VideoPathOpenButton_Click"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<!-- Toggle Settings Grid --> <!-- Toggle Settings Grid -->
<Grid Margin="0,0,0,10"> <Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Visor Wipe Toggle --> <!-- Left Column Controls -->
<StackPanel Grid.Column="0" Grid.Row="0"
Orientation="Horizontal"
Margin="0,0,5,5">
<TextBlock Text="ⓘ"
ToolTip="Perform a Visor Wipe animation on player kill. Requires AHKv2."
Foreground="{DynamicResource TextBrush}"
FontSize="20"
Margin="0,4,3,0"/>
<TextBlock Text="Visor Wipe:"
Foreground="{DynamicResource TextBrush}"
FontSize="16"
Margin="0,7,0,0"/>
<Slider Name="VisorWipeSlider"
Minimum="0"
Maximum="1"
TickFrequency="1"
IsSnapToTickEnabled="True"
Value="0"
Style="{StaticResource ToggleSliderStyle}"
Margin="10,-4,0,0"
Width="100"
ValueChanged="VisorWipeSlider_ValueChanged"/>
</StackPanel>
<!-- Video Record Toggle --> <!-- Visor Wipe Toggle -->
<StackPanel Grid.Column="1" Grid.Row="0" <StackPanel Grid.Column="0"
Orientation="Horizontal" Grid.Row="0"
Margin="5,0,0,5"> Orientation="Horizontal"
<TextBlock Text="ⓘ" Margin="0,0,5,5">
ToolTip="Automatically clip your last kill. Check the README for more info." <TextBlock Text="ⓘ"
Foreground="{DynamicResource TextBrush}" ToolTip="Perform a Visor Wipe animation on player kill. Requires AHKv2."
FontSize="20" Foreground="{DynamicResource TextBrush}"
Margin="0,4,3,0"/> FontSize="20"
<TextBlock Text="Video Record:" Margin="0,4,3,0"/>
Foreground="{DynamicResource TextBrush}" <TextBlock Text="Visor Wipe:"
FontSize="16" Foreground="{DynamicResource TextBrush}"
Margin="0,7,0,0"/> FontSize="16"
<Slider Name="VideoRecordSlider" Margin="0,7,0,0"/>
Minimum="0" <Slider Name="VisorWipeSlider"
Maximum="1" Minimum="0"
TickFrequency="1" Maximum="1"
IsSnapToTickEnabled="True" TickFrequency="1"
Value="0" IsSnapToTickEnabled="True"
Style="{StaticResource ToggleSliderStyle}" Value="0"
Margin="10,-4,0,0" Style="{StaticResource ToggleSliderStyle}"
Width="100" Margin="10,-4,0,0"
ValueChanged="VideoRecordSlider_ValueChanged"/> Width="100"
</StackPanel> ValueChanged="VisorWipeSlider_ValueChanged"/>
</StackPanel>
<!-- Offline Mode Toggle --> <!-- Video Record Toggle -->
<StackPanel Grid.Column="0" Grid.Row="1" <StackPanel Grid.Column="0"
Orientation="Horizontal" Grid.Row="1"
Margin="0,0,5,5"> Orientation="Horizontal"
<TextBlock Text="ⓘ" Margin="0,0,5,5">
ToolTip="With Offline Mode enabled, kills will not be submitted to the configured API." <TextBlock Text="ⓘ"
Foreground="{DynamicResource TextBrush}" ToolTip="Record video on player kill. Requires AHKv2."
FontSize="20" Foreground="{DynamicResource TextBrush}"
Margin="0,4,3,0"/> FontSize="20"
<TextBlock Text="Offline Mode:" Margin="0,4,3,0"/>
Foreground="{DynamicResource TextBrush}" <TextBlock Text="Video Record:"
FontSize="16" Foreground="{DynamicResource TextBrush}"
Margin="0,7,0,0"/> FontSize="16"
<Slider Name="OfflineModeSlider" Margin="0,7,0,0"/>
Minimum="0" <Slider Name="VideoRecordSlider"
Maximum="1" Minimum="0"
TickFrequency="1" Maximum="1"
IsSnapToTickEnabled="True" TickFrequency="1"
Value="0" IsSnapToTickEnabled="True"
Style="{StaticResource ToggleSliderStyle}" Value="0"
Margin="10,-4,0,0" Style="{StaticResource ToggleSliderStyle}"
Width="100" Margin="10,-4,0,0"
ValueChanged="OfflineModeSlider_ValueChanged"/> Width="100"
</StackPanel> ValueChanged="VideoRecordSlider_ValueChanged"/>
</StackPanel>
<!-- Theme Slider --> <!-- Offline Mode Toggle -->
<StackPanel Grid.Column="0" Grid.Row="2" <StackPanel Grid.Column="0"
Grid.ColumnSpan="2" Grid.Row="2"
Orientation="Horizontal" Orientation="Horizontal"
Margin="0,0,0,5"> Margin="0,0,5,5">
<TextBlock Text="Theme:" <TextBlock Text="ⓘ"
Foreground="{DynamicResource TextBrush}" ToolTip="With Offline Mode enabled, kills will not be submitted to the configured API."
FontSize="16" Foreground="{DynamicResource TextBrush}"
Margin="0,7,10,0"/> FontSize="20"
<Slider x:Name="ThemeSlider" Margin="0,4,3,0"/>
Minimum="0" <TextBlock Text="Offline Mode:"
Value="0" Foreground="{DynamicResource TextBrush}"
TickFrequency="1" FontSize="16"
IsSnapToTickEnabled="True" Margin="0,7,0,0"/>
ValueChanged="ThemeSlider_ValueChanged" <Slider Name="OfflineModeSlider"
Width="447" Minimum="0"
Style="{StaticResource ThreePositionSlider}"/> Maximum="1"
</StackPanel> TickFrequency="1"
IsSnapToTickEnabled="True"
Value="0"
Style="{StaticResource ToggleSliderStyle}"
Margin="10,-4,0,0"
Width="100"
ValueChanged="OfflineModeSlider_ValueChanged"/>
</StackPanel>
<!-- Right Column Controls -->
<!-- Streamlink Toggle -->
<StackPanel Grid.Column="1"
Grid.Row="0"
Orientation="Horizontal"
Margin="5,0,0,5">
<TextBlock Text="ⓘ"
ToolTip="Use Streamlink for video recording."
Foreground="{DynamicResource TextBrush}"
FontSize="20"
Margin="0,4,3,0"/>
<TextBlock Text="Streamlink:"
Foreground="{DynamicResource TextBrush}"
FontSize="16"
Margin="0,7,0,0"/>
<Slider Name="StreamlinkSlider"
Minimum="0"
Maximum="1"
TickFrequency="1"
IsSnapToTickEnabled="True"
Value="0"
Style="{StaticResource ToggleSliderStyle}"
Margin="10,-4,0,0"
Width="100"
ValueChanged="StreamlinkSlider_ValueChanged"/>
</StackPanel>
<!-- Streamlink Duration -->
<StackPanel Grid.Column="1"
Grid.Row="1"
Orientation="Horizontal"
Margin="5,0,0,5">
<TextBlock Text="ⓘ"
ToolTip="Duration of Streamlink recordings in seconds."
Foreground="{DynamicResource TextBrush}"
FontSize="20"
Margin="0,4,3,0"/>
<TextBlock Text="Duration:"
Foreground="{DynamicResource TextBrush}"
FontSize="16"
Margin="0,7,0,0"/>
<Slider Name="StreamlinkDurationSlider"
Minimum="10"
Maximum="600"
TickFrequency="30"
IsSnapToTickEnabled="True"
Value="30"
Style="{StaticResource SliderStyle}"
Margin="10,-4,0,0"
Width="100"
ValueChanged="StreamlinkDurationSlider_ValueChanged"/>
<TextBlock Name="StreamlinkDurationText"
Text="30s"
Foreground="{DynamicResource TextBrush}"
FontSize="16"
Margin="5,7,0,0"/>
</StackPanel>
<!-- Test Streamlink Button -->
<Button Grid.Column="1"
Grid.Row="2"
Content="Test Streamlink"
Width="120"
Height="30"
FontFamily="{StaticResource Orbitron}"
Margin="5,0,0,5"
Style="{StaticResource ButtonStyle}"
Click="TestStreamlinkButton_Click"/>
<!-- Theme Slider -->
<StackPanel Grid.Column="0"
Grid.Row="3"
Grid.ColumnSpan="2"
Orientation="Horizontal"
Margin="0,0,0,5">
<TextBlock Text="Theme:"
Foreground="{DynamicResource TextBrush}"
FontSize="16"
Margin="0,7,10,0"/>
<Slider x:Name="ThemeSlider"
Minimum="0"
Value="0"
TickFrequency="1"
IsSnapToTickEnabled="True"
ValueChanged="ThemeSlider_ValueChanged"
Width="350"
Style="{StaticResource ThreePositionSlider}"/>
</StackPanel>
</Grid> </Grid>
</StackPanel> </StackPanel>
<!-- Save Button --> <!-- Save Button -->
<StackPanel Grid.Column="2" <Button x:Name="SaveButton"
HorizontalAlignment="Right" Grid.Row="1"
VerticalAlignment="Bottom" Content="Save"
Margin="0,0,0,10"> Width="100"
<Button x:Name="SaveButton" Height="40"
Content="Save" Style="{StaticResource ButtonStyle}"
Width="100" FontFamily="{StaticResource Orbitron}"
Height="40" HorizontalAlignment="Right"
Style="{StaticResource ButtonStyle}" VerticalAlignment="Bottom"
FontFamily="{StaticResource Orbitron}" Margin="0,0,80,20"
Click="SaveButton_Click"/> Click="SaveButton_Click"/>
</StackPanel>
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -11,6 +11,7 @@ using System.Windows.Media;
using System.Windows.Media.Effects; using System.Windows.Media.Effects;
using System.Windows.Threading; using System.Windows.Threading;
using Microsoft.Win32; using Microsoft.Win32;
using System.Threading.Tasks;
namespace AutoTrackR2; namespace AutoTrackR2;
@ -36,6 +37,18 @@ public partial class ConfigPage : UserControl
VideoRecordSlider.Value = ConfigManager.VideoRecord; VideoRecordSlider.Value = ConfigManager.VideoRecord;
OfflineModeSlider.Value = ConfigManager.OfflineMode; OfflineModeSlider.Value = ConfigManager.OfflineMode;
ThemeSlider.Value = ConfigManager.Theme; ThemeSlider.Value = ConfigManager.Theme;
StreamlinkSlider.Value = ConfigManager.StreamlinkEnabled;
StreamlinkDurationSlider.Value = ConfigManager.StreamlinkDuration;
// Initialize Streamlink slider style
if (StreamlinkSlider.Value == 0)
{
StreamlinkSlider.Style = (Style)Application.Current.FindResource("FalseToggleStyle");
}
else
{
StreamlinkSlider.Style = (Style)Application.Current.FindResource("ToggleSliderStyle");
}
ApplyToggleModeStyle(OfflineModeSlider.Value, VisorWipeSlider.Value, VideoRecordSlider.Value); ApplyToggleModeStyle(OfflineModeSlider.Value, VisorWipeSlider.Value, VideoRecordSlider.Value);
@ -213,51 +226,6 @@ public partial class ConfigPage : UserControl
} }
} }
private void VisorWipeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
Slider slider = (Slider)sender;
// Build the dynamic file path for the current user
if (string.IsNullOrEmpty(ConfigManager.AHKScriptFolder))
{
MessageBox.Show("AHK script folder path is not configured.", "Configuration Error", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
string filePath = Path.Combine(
ConfigManager.AHKScriptFolder,
"visorwipe.ahk"
);
// Get the current value of the slider (0 or 1)
ConfigManager.VisorWipe = (int)slider.Value;
if (ConfigManager.VisorWipe == 1)
{
// Check if the file exists
if (File.Exists(filePath))
{
// Apply the enabled style if the file exists
slider.Style = (Style)Application.Current.FindResource("ToggleSliderStyle");
}
else
{
// File does not exist; revert the toggle to 0
ConfigManager.VisorWipe = 0;
slider.Value = 0; // Revert the slider value
slider.Style = (Style)Application.Current.FindResource("FalseToggleStyle");
// Optionally, display a message to the user
MessageBox.Show($"Visor wipe script not found. Please ensure the file exists at:\n{filePath}",
"File Missing", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
else
{
// Apply the disabled style
slider.Style = (Style)Application.Current.FindResource("FalseToggleStyle");
}
}
private void VideoRecordSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) private void VideoRecordSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{ {
Slider slider = (Slider)sender; Slider slider = (Slider)sender;
@ -315,6 +283,51 @@ public partial class ConfigPage : UserControl
} }
} }
private void VisorWipeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
Slider slider = (Slider)sender;
// Build the dynamic file path for the current user
if (string.IsNullOrEmpty(ConfigManager.AHKScriptFolder))
{
MessageBox.Show("AHK script folder path is not configured.", "Configuration Error", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
string filePath = Path.Combine(
ConfigManager.AHKScriptFolder,
"visorwipe.ahk"
);
// Get the current value of the slider (0 or 1)
ConfigManager.VisorWipe = (int)slider.Value;
if (ConfigManager.VisorWipe == 1)
{
// Check if the file exists
if (File.Exists(filePath))
{
// Apply the enabled style if the file exists
slider.Style = (Style)Application.Current.FindResource("ToggleSliderStyle");
}
else
{
// File does not exist; revert the toggle to 0
ConfigManager.VisorWipe = 0;
slider.Value = 0; // Revert the slider value
slider.Style = (Style)Application.Current.FindResource("FalseToggleStyle");
// Optionally, display a message to the user
MessageBox.Show($"Visor wipe script not found. Please ensure the file exists at:\n{filePath}",
"File Missing", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
else
{
// Apply the disabled style
slider.Style = (Style)Application.Current.FindResource("FalseToggleStyle");
}
}
private void SaveButton_Click(object sender, RoutedEventArgs e) private void SaveButton_Click(object sender, RoutedEventArgs e)
{ {
ConfigManager.ApiKey = ApiKey.Password; ConfigManager.ApiKey = ApiKey.Password;
@ -422,6 +435,133 @@ public partial class ConfigPage : UserControl
MessageBox.Show($"API Test Failure. {ex.Message}"); MessageBox.Show($"API Test Failure. {ex.Message}");
} }
} }
private void StreamlinkSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
Slider slider = (Slider)sender;
// Only allow enabling if streamlink is installed
if (slider.Value == 1)
{
if (!StreamlinkHandler.IsStreamlinkInstalled())
{
MessageBox.Show("Streamlink is not installed. Please install Streamlink to use this feature.",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
slider.Value = 0;
return;
}
}
ConfigManager.StreamlinkEnabled = (int)slider.Value;
// Apply the appropriate style based on the value
if (slider.Value == 0)
{
slider.Style = (Style)Application.Current.FindResource("FalseToggleStyle");
}
else
{
slider.Style = (Style)Application.Current.FindResource("ToggleSliderStyle");
}
}
private void StreamlinkDurationSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (StreamlinkDurationText != null)
{
int duration = (int)e.NewValue;
StreamlinkDurationText.Text = $"{duration}s";
ConfigManager.StreamlinkDuration = duration;
}
}
private async void TestStreamlinkButton_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("TestStreamlinkButton_Click: Starting streamlink test");
if (ConfigManager.StreamlinkEnabled != 1)
{
Debug.WriteLine("TestStreamlinkButton_Click: Streamlink is not enabled");
MessageBox.Show("Streamlink is not enabled. Please enable Streamlink to test.", "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
Debug.WriteLine("TestStreamlinkButton_Click: Configuring HTTP client");
// Configure HttpClient with TLS 1.2
var handler = new HttpClientHandler
{
SslProtocols = System.Security.Authentication.SslProtocols.Tls12
};
using (var client = new HttpClient(handler))
{
// Set headers
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ConfigManager.ApiKey);
client.DefaultRequestHeaders.UserAgent.ParseAdd("AutoTrackR2");
// Create JSON body with version
var jsonBody = new { version = "2.10" };
var content = new StringContent(JsonSerializer.Serialize(jsonBody), Encoding.UTF8, "application/json");
// Send POST to test endpoint
string baseUrl = Regex.Replace(ConfigManager.ApiUrl ?? "", @"(https?://[^/]+)/?.*", "$1");
string endpoint = "test";
string fullUrl = $"{baseUrl}/{endpoint}";
Debug.WriteLine($"TestStreamlinkButton_Click: Sending request to {fullUrl}");
var response = await client.PostAsync(fullUrl, content);
Debug.WriteLine($"TestStreamlinkButton_Click: Response status code: {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
Debug.WriteLine($"TestStreamlinkButton_Click: Response content: {responseContent}");
WebHandler.ProcessStreamerResponse(responseContent);
MessageBox.Show("Streamlink Test Success.");
}
else
{
Debug.WriteLine($"TestStreamlinkButton_Click: Error response: {response.StatusCode} - {response.ReasonPhrase}");
MessageBox.Show($"Error: {response.StatusCode} - {response.ReasonPhrase}");
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"TestStreamlinkButton_Click: Exception: {ex.Message}");
MessageBox.Show($"Streamlink Test Failure. {ex.Message}");
}
}
private void LogFileOpenButton_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(LogFilePath.Text))
{
string directory = Path.GetDirectoryName(LogFilePath.Text) ?? string.Empty;
if (Directory.Exists(directory))
{
Process.Start("explorer.exe", directory);
}
else
{
MessageBox.Show("Directory does not exist.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
private void VideoPathOpenButton_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(VideoPath.Text) && Directory.Exists(VideoPath.Text))
{
Process.Start("explorer.exe", VideoPath.Text);
}
else
{
MessageBox.Show("Directory does not exist.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
} }
public class Theme public class Theme

View file

@ -196,6 +196,8 @@ namespace AutoTrackR2
public static int VideoRecord { get; set; } public static int VideoRecord { get; set; }
public static int OfflineMode { get; set; } public static int OfflineMode { get; set; }
public static int Theme { get; set; } public static int Theme { get; set; }
public static int StreamlinkEnabled { get; set; }
public static int StreamlinkDuration { get; set; } = 30;
static ConfigManager() static ConfigManager()
{ {
@ -247,6 +249,10 @@ namespace AutoTrackR2
OfflineMode = int.Parse(line.Substring("OfflineMode=".Length).Trim()); OfflineMode = int.Parse(line.Substring("OfflineMode=".Length).Trim());
else if (line.StartsWith("Theme=")) else if (line.StartsWith("Theme="))
Theme = int.Parse(line.Substring("Theme=".Length).Trim()); Theme = int.Parse(line.Substring("Theme=".Length).Trim());
else if (line.StartsWith("StreamlinkEnabled="))
StreamlinkEnabled = int.Parse(line.Substring("StreamlinkEnabled=".Length).Trim());
else if (line.StartsWith("StreamlinkDuration="))
StreamlinkDuration = int.Parse(line.Substring("StreamlinkDuration=".Length).Trim());
} }
} }
} }
@ -278,6 +284,8 @@ namespace AutoTrackR2
writer.WriteLine($"VideoRecord={VideoRecord}"); writer.WriteLine($"VideoRecord={VideoRecord}");
writer.WriteLine($"OfflineMode={OfflineMode}"); writer.WriteLine($"OfflineMode={OfflineMode}");
writer.WriteLine($"Theme={Theme}"); writer.WriteLine($"Theme={Theme}");
writer.WriteLine($"StreamlinkEnabled={StreamlinkEnabled}");
writer.WriteLine($"StreamlinkDuration={StreamlinkDuration}");
} }
} }
} }

View file

@ -0,0 +1,88 @@
using System.Diagnostics;
using System.IO;
namespace AutoTrackR2;
public class StreamlinkHandler
{
public StreamlinkHandler()
{
TrackREventDispatcher.StreamlinkRecordEvent += HandleStreamlinkRecord;
}
public static bool IsStreamlinkInstalled()
{
try
{
var checkInfo = new ProcessStartInfo
{
FileName = "where",
Arguments = "streamlink",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
using (var process = Process.Start(checkInfo))
{
if (process == null)
{
return false;
}
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return process.ExitCode == 0 && !string.IsNullOrEmpty(output);
}
}
catch
{
return false;
}
}
private async void HandleStreamlinkRecord(string streamerHandle)
{
if (ConfigManager.StreamlinkEnabled != 1)
{
return;
}
try
{
var outputPath = Path.Combine(
ConfigManager.VideoPath ?? Environment.GetFolderPath(Environment.SpecialFolder.MyVideos),
$"kill_{DateTime.Now:yyyyMMdd_HHmmss}.mp4"
);
// Calculate the duration for recording (30 seconds before + configured duration after)
var totalDuration = 30 + ConfigManager.StreamlinkDuration;
var recordInfo = new ProcessStartInfo
{
FileName = "streamlink",
Arguments = $"https://www.twitch.tv/{streamerHandle} best --hls-live-edge 30 --hls-duration {totalDuration} -o {outputPath}",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
using (var process = Process.Start(recordInfo))
{
if (process == null)
{
return;
}
// Wait for the recording to complete
await process.WaitForExitAsync();
}
}
catch (Exception ex)
{
// Log the error but don't crash the application
Debug.WriteLine($"Error recording streamlink clip: {ex.Message}");
}
}
}

View file

@ -54,4 +54,11 @@ public static class TrackREventDispatcher
{ {
JumpDriveStateChangedEvent?.Invoke(data); JumpDriveStateChangedEvent?.Invoke(data);
} }
public static event Action<string>? StreamlinkRecordEvent;
public static void OnStreamlinkRecordEvent(string streamerHandle)
{
StreamlinkRecordEvent?.Invoke(streamerHandle);
}
} }

View file

@ -155,7 +155,12 @@ public static class WebHandler
Console.WriteLine($"Time (UTC): {DateTimeOffset.UtcNow}"); Console.WriteLine($"Time (UTC): {DateTimeOffset.UtcNow}");
Console.WriteLine("=== End Debug Info ===\n"); Console.WriteLine("=== End Debug Info ===\n");
var response = await httpClient.PostAsync(ConfigManager.ApiUrl + "register-kill", new StringContent(jsonData, Encoding.UTF8, "application/json")); // Ensure proper URL formatting
string baseUrl = Regex.Replace(ConfigManager.ApiUrl ?? "", @"(https?://[^/]+)/?.*", "$1");
string endpoint = "register-kill";
string fullUrl = $"{baseUrl}/{endpoint}";
var response = await httpClient.PostAsync(fullUrl, new StringContent(jsonData, Encoding.UTF8, "application/json"));
if (response.StatusCode != HttpStatusCode.OK) if (response.StatusCode != HttpStatusCode.OK)
{ {
Console.WriteLine("Failed to submit kill data:"); Console.WriteLine("Failed to submit kill data:");
@ -167,7 +172,50 @@ public static class WebHandler
else if (response.StatusCode == HttpStatusCode.OK) else if (response.StatusCode == HttpStatusCode.OK)
{ {
Console.WriteLine("Successfully submitted kill data"); Console.WriteLine("Successfully submitted kill data");
Console.WriteLine($"Response: {await response.Content.ReadAsStringAsync()}"); var responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Response: {responseContent}");
// Only process streamer data if streamlink is enabled
if (ConfigManager.StreamlinkEnabled == 1)
{
ProcessStreamerResponse(responseContent);
}
} }
} }
public static void ProcessStreamerResponse(string responseContent)
{
try
{
var responseData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(responseContent);
if (responseData != null && responseData.TryGetValue("streamer", out JsonElement streamerElement))
{
string streamerHandle = streamerElement.GetString() ?? string.Empty;
if (!string.IsNullOrEmpty(streamerHandle))
{
// Sanitize the streamer handle before using it, this is to prevent any malicious instructions.
string sanitizedHandle = SanitizeStreamerHandle(streamerHandle);
if (!string.IsNullOrEmpty(sanitizedHandle))
{
TrackREventDispatcher.OnStreamlinkRecordEvent(sanitizedHandle);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error parsing streamer from response: {ex.Message}");
}
}
private static string SanitizeStreamerHandle(string handle)
{
// Twitch usernames 4-25 characters, letters, numbers and underscores.
if (Regex.IsMatch(handle, @"^[a-zA-Z0-9_]{4,25}$"))
{
return handle.ToLower(); // Api won't return anything other than lowercase but just in case.
}
return string.Empty; // Reject invalid handles
}
} }

View file

@ -6,3 +6,4 @@ VisorWipe=0
VideoRecord=0 VideoRecord=0
OfflineMode=0 OfflineMode=0
Theme=0 Theme=0
StreamlinkEnabled=0