Work done so far

This commit is contained in:
minster586
2026-08-11 22:42:01 -04:00
parent da5fb64400
commit 02bd8ffd5e
10 changed files with 1321 additions and 256 deletions
+182
View File
@@ -0,0 +1,182 @@
# Kill Process on Disconnect Feature - Implementation Summary
## Overview
This document describes the implementation of the "Kill Process on Disconnect" feature for the Pianobar Helper application.
## Feature Requirements
1. **Kill Process Checkbox (`chkkillpro`)**: A checkbox in the Settings form that allows users to enable/disable the kill-on-disconnect behavior
2. **Gating Mechanism**: The checkbox must be disabled (non-interactive) unless the "Enable Profile Credentials and Custom Config Generation" checkbox (`chkOverrideCredentials`) is checked
3. **Disconnect Behavior**: When the user clicks the Disconnect button in the main application:
- If `chkkillpro` is checked: Pianobar process is immediately killed via its PID
- If `chkkillpro` is unchecked: Pianobar process is detached (continues running in background)
## Implementation Details
### 1. Data Model (`ProfileSettings`)
**File**: `ProfileManager.cs`
Added `KillProcessOnDisconnect` boolean property to `ProfileSettings`:
```csharp
public class ProfileSettings
{
// ... other properties ...
public bool KillProcessOnDisconnect { get; set; }
}
```
This property is saved/loaded with each profile in `profiles.json`.
### 2. Process Management (`PianobarManager`)
**File**: `PianobarManager.cs`
Implemented two disconnect methods:
#### `Disconnect()` - Detach mode
- Cleans up streams and event handlers
- Sets `IsConnected = false`
- Leaves the Pianobar process running in the background
#### `DisconnectAndKill()` - Kill mode
- Checks if the process exists and is not exited
- Calls `Process.Kill()` to terminate immediately
- Reports the PID that was killed
- Cleans up and sets `IsConnected = false`
### 3. Settings UI (`SettingsForm`)
**File**: `Settings.cs` and `Settings.Designer.cs`
#### Control Initialization
- `chkkillpro` checkbox starts **disabled** (set in Designer)
- All credential-related fields start disabled:
- `txtConfigFilePath`
- `btnBrowseConfigFile`
- `txtPianobarUser`
- `txtPianobarPass`
- `chkkillpro`
#### Event Handling
**`chkOverrideCredentials_CheckedChanged`**:
- Wired in Designer (line 352)
- Calls `ToggleCredentialFields(checked)` method
**`ToggleCredentialFields(bool enabled)`**:
```csharp
private void ToggleCredentialFields(bool enabled)
{
if (txtConfigFilePath != null)
txtConfigFilePath.Enabled = enabled;
if (btnBrowseConfigFile != null)
btnBrowseConfigFile.Enabled = enabled;
if (txtPianobarUser != null)
txtPianobarUser.Enabled = enabled;
if (txtPianobarPass != null)
txtPianobarPass.Enabled = enabled;
if (chkkillpro != null)
chkkillpro.Enabled = enabled; // <-- Critical: Kill checkbox is gated here
}
```
#### Profile Save/Load
- `LoadSettingsToControls()` loads `KillProcessOnDisconnect` into `chkkillpro`
- `SaveSettingsFromControls()` saves `chkkillpro.Checked` to profile settings
- When saving, the profile is set as active via `SetActiveProfile()`
### 4. Main Application Logic (`Main.cs`)
**File**: `Main.cs`
**`btnDisconnect_Click` handler**:
```csharp
public void btnDisconnect_Click(object? sender, EventArgs e)
{
if (!_pianobarManager.IsConnected)
{
MessageBox.Show("Not connected to Pianobar.", "Not Connected",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (statusLabelRight != null)
statusLabelRight.Text = "Disconnecting...";
// Check if we should kill the process or just detach
var activeProfile = _profileManager.GetActiveProfile();
bool killProcess = activeProfile?.Settings.KillProcessOnDisconnect ?? false;
if (killProcess)
{
_pianobarManager.DisconnectAndKill(); // <-- Terminates process
}
else
{
_pianobarManager.Disconnect(); // <-- Detaches, leaves running
}
_outputManager?.Stop();
}
```
### 5. Output Folder Browse Button
**File**: `Settings.cs` and `Settings.Designer.cs`
**`output_folder_browse_button`** (ID as requested):
- Wired in Designer to `output_folder_browse_button_Click` (line 237)
- Opens `SaveFileDialog` to allow user to select output file location
- Default filename: `now_playing.txt`
- Selected path is saved to `output_folder_text_box` (maps to `TextFilePath` in settings)
## User Workflow
### Enabling Kill-on-Disconnect
1. Open **File ? Settings**
2. Navigate to **"Pianobar Config & Auth"** tab
3. Check **"Enable Profile Credentials and Custom Config Generation"** (`chkOverrideCredentials`)
- This enables all credential fields **and** the kill process checkbox
4. Check **"Kill process"** (`chkkillpro`)
5. Click **Save**
- Profile is updated and set as active
- Setting persists for future sessions
### Using the Feature
- **Connect**: Click Connect button (Pianobar starts in background, PID shown in status)
- **Disconnect with Kill**: If `chkkillpro` was checked:
- Pianobar process is immediately terminated by PID
- Status shows "Pianobar process killed (PID: XXXX)"
- **Disconnect without Kill**: If `chkkillpro` was unchecked:
- Pianobar process continues running
- Application detaches and closes streams
- Status shows "Disconnected from Pianobar (process detached)"
### Disabling Override
- If user unchecks `chkOverrideCredentials`:
- Kill process checkbox becomes disabled and cannot be checked
- All credential fields are disabled
- User cannot enable kill-on-disconnect without also enabling custom config generation
## Testing Checklist
? **Build Status**: Project builds successfully without errors
? **Designer Wiring**: All event handlers properly wired in Designer files:
- `chkOverrideCredentials.CheckedChanged` ? `chkOverrideCredentials_CheckedChanged`
- `output_folder_browse_button.Click` ? `output_folder_browse_button_Click`
- `chkkillpro` exists and is added to `pianobar_override_tab_page`
? **Initial State**: All credential fields and `chkkillpro` start disabled
? **Gating Logic**: `ToggleCredentialFields()` enables/disables `chkkillpro` based on override checkbox
? **Profile Persistence**: `KillProcessOnDisconnect` saved/loaded with profile
? **Disconnect Logic**: Main form branches to kill or detach based on profile setting
? **Process Management**: `DisconnectAndKill()` terminates by PID; `Disconnect()` detaches only
## Files Modified
1. **ProfileManager.cs** - Added `KillProcessOnDisconnect` to data model
2. **PianobarManager.cs** - Added `DisconnectAndKill()` method
3. **Settings.cs** - Load/save logic and gating via `ToggleCredentialFields()`
4. **Settings.Designer.cs** - Set initial `Enabled = false` for credential fields and `chkkillpro`
5. **Main.cs** - Disconnect logic branches based on profile flag
## Summary
The kill-on-disconnect feature is fully implemented and integrated with the existing profile system. The checkbox is properly gated by the override credentials checkbox, ensuring users cannot accidentally enable the feature without understanding its context within custom config generation. The feature provides clean process termination when desired while preserving the option to keep Pianobar running in the background.
+92 -7
View File
@@ -30,6 +30,26 @@ namespace PainoBar_Helper
InitializeEvents();
UpdateProfilesMenu();
UpdateConnectionStatus(ConnectionStatus.Disconnected);
// Set initial status
if (statusLabelRight != null)
{
var activeProfile = _profileManager.GetActiveProfile();
if (activeProfile != null)
{
statusLabelRight.Text = $"Ready - Profile: {activeProfile.Name}";
}
else if (_profileManager.Profiles.Any())
{
// If no active profile but profiles exist, set the first one as active
_profileManager.SetActiveProfile(_profileManager.Profiles.First().Name);
statusLabelRight.Text = $"Ready - Profile: {_profileManager.ActiveProfileName}";
}
else
{
statusLabelRight.Text = "Ready - No profile configured";
}
}
}
#region Initialization
@@ -83,18 +103,58 @@ namespace PainoBar_Helper
return;
}
// Get pianobar.exe path from active profile or use default
var activeProfile = _profileManager.GetActiveProfile();
if (activeProfile == null)
{
var result = MessageBox.Show(
"No profile selected. Would you like to create one now?\n\n" +
"Click Yes to open Settings, or No to cancel.",
"No Profile",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
menuSettings_Click(sender, e);
}
return;
}
string pianobarPath = activeProfile.Settings.PianobarPath;
string? configPath = _profileManager.GetActiveConfigPath();
string? eventFilePath = _profileManager.GetEventOutputPath();
// Debug information
string debugInfo = $"Profile: {activeProfile.Name}\n" +
$"Pianobar Path: {pianobarPath}\n" +
$"Config Path: {configPath ?? "(none)"}\n" +
$"Event File: {eventFilePath ?? "(none)"}\n" +
$"File Exists: {File.Exists(pianobarPath)}";
try
{
// Get pianobar.exe path (you may want to add a setting for this)
string pianobarPath = "pianobar.exe"; // Default, assumes it's in PATH
string? configPath = _profileManager.GetActiveConfigPath();
// Update status to show we're attempting connection
if (statusLabelRight != null)
statusLabelRight.Text = $"Starting Pianobar...";
_pianobarManager.Connect(pianobarPath, configPath);
_pianobarManager.Connect(pianobarPath, configPath, eventFilePath);
// Success message will be shown by the Connected event handler
}
catch (Exception ex)
{
MessageBox.Show($"Failed to connect to Pianobar:\n{ex.Message}", "Connection Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
// Update status to show connection failed
if (statusLabelRight != null)
statusLabelRight.Text = "Failed to start Pianobar";
MessageBox.Show(
$"Failed to start Pianobar:\n\n{ex.Message}\n\n" +
$"Debug Information:\n{debugInfo}",
"Connection Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
@@ -108,7 +168,22 @@ namespace PainoBar_Helper
return;
}
if (statusLabelRight != null)
statusLabelRight.Text = "Disconnecting...";
// Check if we should kill the process or just detach
var activeProfile = _profileManager.GetActiveProfile();
bool killProcess = activeProfile?.Settings.KillProcessOnDisconnect ?? false;
if (killProcess)
{
_pianobarManager.DisconnectAndKill();
}
else
{
_pianobarManager.Disconnect();
}
_outputManager?.Stop();
}
@@ -180,7 +255,17 @@ namespace PainoBar_Helper
var settingsForm = new SettingsForm(_profileManager, activeProfile?.Settings ?? new ProfileSettings());
if (settingsForm.ShowDialog(this) == DialogResult.OK)
{
// Settings were saved, restart output manager if connected
// Settings were saved, update the UI
UpdateProfilesMenu();
// Update status to show the new active profile
var newActiveProfile = _profileManager.GetActiveProfile();
if (newActiveProfile != null && statusLabelRight != null)
{
statusLabelRight.Text = $"Profile saved: {newActiveProfile.Name}";
}
// Restart output manager if connected
if (_pianobarManager.IsConnected)
{
RestartOutputManager();
+132
View File
@@ -0,0 +1,132 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace PainoBar_Helper
{
/// <summary>
/// Monitors Pianobar event output file for song changes and metadata updates
/// </summary>
public class PianobarEventMonitor : IDisposable
{
private FileSystemWatcher? _watcher;
private string? _eventFilePath;
private bool _disposed;
public event EventHandler<PianobarEventData>? EventReceived;
public void StartMonitoring(string eventFilePath)
{
if (string.IsNullOrEmpty(eventFilePath))
throw new ArgumentException("Event file path cannot be empty", nameof(eventFilePath));
StopMonitoring();
_eventFilePath = eventFilePath;
// Ensure the directory exists
var directory = Path.GetDirectoryName(eventFilePath);
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
// Create initial file if it doesn't exist
if (!File.Exists(eventFilePath))
File.WriteAllText(eventFilePath, "{}");
// Set up file watcher
_watcher = new FileSystemWatcher
{
Path = directory!,
Filter = Path.GetFileName(eventFilePath),
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size
};
_watcher.Changed += OnFileChanged;
_watcher.EnableRaisingEvents = true;
}
public void StopMonitoring()
{
if (_watcher != null)
{
_watcher.EnableRaisingEvents = false;
_watcher.Changed -= OnFileChanged;
_watcher.Dispose();
_watcher = null;
}
}
private void OnFileChanged(object sender, FileSystemEventArgs e)
{
try
{
// Give the file a moment to finish writing
Thread.Sleep(50);
if (string.IsNullOrEmpty(_eventFilePath) || !File.Exists(_eventFilePath))
return;
string json = File.ReadAllText(_eventFilePath);
if (string.IsNullOrWhiteSpace(json))
return;
var eventData = JsonSerializer.Deserialize<PianobarEventData>(json);
if (eventData != null)
{
EventReceived?.Invoke(this, eventData);
}
}
catch
{
// Ignore file read errors (file might be locked, etc.)
}
}
public void Dispose()
{
if (_disposed)
return;
StopMonitoring();
_disposed = true;
}
}
/// <summary>
/// Represents event data from Pianobar
/// </summary>
public class PianobarEventData
{
[JsonPropertyName("event")]
public string Event { get; set; } = string.Empty;
[JsonPropertyName("timestamp")]
public string Timestamp { get; set; } = string.Empty;
[JsonPropertyName("artist")]
public string Artist { get; set; } = string.Empty;
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("album")]
public string Album { get; set; } = string.Empty;
[JsonPropertyName("coverArt")]
public string CoverArt { get; set; } = string.Empty;
[JsonPropertyName("stationName")]
public string StationName { get; set; } = string.Empty;
[JsonPropertyName("songDuration")]
public string SongDuration { get; set; } = string.Empty;
[JsonPropertyName("songPlayed")]
public string SongPlayed { get; set; } = string.Empty;
[JsonPropertyName("rating")]
public string Rating { get; set; } = string.Empty;
[JsonPropertyName("detailUrl")]
public string DetailUrl { get; set; } = string.Empty;
}
}
+242 -9
View File
@@ -13,6 +13,13 @@ namespace PainoBar_Helper
private StreamWriter? _inputWriter;
private bool _isConnected;
private readonly object _lockObject = new object();
private PianobarEventMonitor? _eventMonitor;
private bool _receivedStdOut;
private bool _receivedStdErr;
private readonly string _ioLogPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"PianobarHelper",
"pianobar-io.log");
public event EventHandler<SongChangedEventArgs>? SongChanged;
public event EventHandler<string>? StationChanged;
@@ -52,32 +59,68 @@ namespace PainoBar_Helper
public string? CurrentStation { get; private set; }
public bool IsPlaying { get; private set; } = true;
public void Connect(string pianobarPath, string? configPath = null)
public void Connect(string pianobarPath, string? configPath = null, string? eventFilePath = null)
{
if (IsConnected)
throw new InvalidOperationException("Already connected to Pianobar.");
// Check if pianobar is already running
_receivedStdOut = false;
_receivedStdErr = false;
// Check if pianobar is already running (check both with and without .exe)
var existingProcesses = Process.GetProcessesByName("pianobar");
if (existingProcesses.Length == 0)
{
// Also try without extension
string processName = Path.GetFileNameWithoutExtension(pianobarPath);
existingProcesses = Process.GetProcessesByName(processName);
}
if (existingProcesses.Length > 0)
{
foreach (var proc in existingProcesses)
proc.Dispose();
throw new InvalidOperationException("Pianobar is already running. Please close existing instances.");
throw new InvalidOperationException("Pianobar is already running. Please close existing instances before connecting.");
}
// Validate the pianobar path exists
string fullPath = pianobarPath;
// If it's just a filename or not found, try to find it in PATH
if (!File.Exists(fullPath))
{
string? foundPath = FindInPath(pianobarPath);
if (!string.IsNullOrEmpty(foundPath))
{
fullPath = foundPath;
}
else
{
throw new FileNotFoundException(
$"Pianobar executable not found.\n\n" +
$"Path checked: {pianobarPath}\n" +
$"Full path: {Path.GetFullPath(pianobarPath)}\n\n" +
$"Please verify:\n" +
$"1. The path in Settings is correct\n" +
$"2. Pianobar is installed\n" +
$"3. The file exists at the specified location");
}
}
try
{
var startInfo = new ProcessStartInfo
{
FileName = pianobarPath,
FileName = fullPath,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
CreateNoWindow = true, // Run in background without terminal window
WindowStyle = ProcessWindowStyle.Hidden,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
StandardErrorEncoding = Encoding.UTF8,
WorkingDirectory = Path.GetDirectoryName(fullPath) ?? Environment.CurrentDirectory
};
if (!string.IsNullOrEmpty(configPath))
@@ -91,13 +134,35 @@ namespace PainoBar_Helper
_process.Exited += Process_Exited;
_process.EnableRaisingEvents = true;
_process.Start();
bool started = _process.Start();
if (!started)
{
throw new InvalidOperationException("Process.Start() returned false. The process could not be started.");
}
// Verify the process actually started
if (_process.HasExited)
{
throw new InvalidOperationException($"Process started but immediately exited with code: {_process.ExitCode}");
}
_inputWriter = _process.StandardInput;
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
IsConnected = true;
RaiseEvent("Connected to Pianobar");
// Start monitoring event file if provided
if (!string.IsNullOrEmpty(eventFilePath))
{
_eventMonitor = new PianobarEventMonitor();
_eventMonitor.EventReceived += OnEventReceived;
_eventMonitor.StartMonitoring(eventFilePath);
}
RaiseEvent($"Pianobar started successfully (PID: {_process.Id})");
RaiseEvent($"I/O log: {_ioLogPath}");
}
catch (Exception ex)
{
@@ -106,6 +171,59 @@ namespace PainoBar_Helper
}
}
private string? FindInPath(string command)
{
// Check if the command is available in PATH
var pathEnv = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(pathEnv))
return null;
var paths = pathEnv.Split(Path.PathSeparator);
var extensions = new[] { ".exe", ".cmd", ".bat", "" };
// Remove .exe if already present in command
string baseCommand = Path.GetFileNameWithoutExtension(command);
string commandExt = Path.GetExtension(command);
foreach (var path in paths)
{
try
{
if (!Directory.Exists(path))
continue;
foreach (var ext in extensions)
{
// If command already has extension, use it as-is
if (!string.IsNullOrEmpty(commandExt))
{
var fullPath = Path.Combine(path, command);
if (File.Exists(fullPath))
return fullPath;
}
else
{
var fullPath = Path.Combine(path, baseCommand + ext);
if (File.Exists(fullPath))
return fullPath;
}
}
}
catch
{
// Skip paths that cause errors
continue;
}
}
return null;
}
private bool IsCommandInPath(string command)
{
return !string.IsNullOrEmpty(FindInPath(command));
}
public void Disconnect()
{
if (!IsConnected)
@@ -113,7 +231,33 @@ namespace PainoBar_Helper
Cleanup();
IsConnected = false;
RaiseEvent("Disconnected from Pianobar");
RaiseEvent("Disconnected from Pianobar (process detached)");
}
public void DisconnectAndKill()
{
if (!IsConnected)
return;
try
{
if (_process != null && !_process.HasExited)
{
int pid = _process.Id;
_process.Kill();
_process.WaitForExit(5000); // Wait up to 5 seconds for graceful exit
RaiseEvent($"Pianobar process killed (PID: {pid})");
}
}
catch (Exception ex)
{
RaiseEvent($"Error killing process: {ex.Message}");
}
finally
{
Cleanup();
IsConnected = false;
}
}
public void SendCommand(char command)
@@ -172,6 +316,13 @@ namespace PainoBar_Helper
return;
string line = e.Data;
if (!_receivedStdOut)
{
_receivedStdOut = true;
RaiseEvent("Received output on STDOUT");
}
LogIoLine("STDOUT", line);
OutputReceived?.Invoke(this, line);
ParseOutput(line);
@@ -180,15 +331,44 @@ namespace PainoBar_Helper
private void Process_ErrorDataReceived(object? sender, DataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data))
{
if (!_receivedStdErr)
{
_receivedStdErr = true;
RaiseEvent("Received output on STDERR");
}
LogIoLine("STDERR", e.Data);
OutputReceived?.Invoke(this, e.Data);
ParseOutput(e.Data);
}
}
private void Process_Exited(object? sender, EventArgs e)
{
if (!_receivedStdOut)
RaiseEvent("No STDOUT data received before process exit");
IsConnected = false;
RaiseEvent("Pianobar process exited");
}
private void LogIoLine(string source, string line)
{
try
{
var logDir = Path.GetDirectoryName(_ioLogPath);
if (!string.IsNullOrEmpty(logDir))
Directory.CreateDirectory(logDir);
File.AppendAllText(_ioLogPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{source}] {line}{Environment.NewLine}");
}
catch
{
// Ignore logging errors
}
}
private void ParseOutput(string line)
{
try
@@ -278,6 +458,51 @@ namespace PainoBar_Helper
}
}
private void OnEventReceived(object? sender, PianobarEventData eventData)
{
// Update metadata from event file
if (!string.IsNullOrEmpty(eventData.Title))
CurrentSong = eventData.Title;
if (!string.IsNullOrEmpty(eventData.Artist))
CurrentArtist = eventData.Artist;
if (!string.IsNullOrEmpty(eventData.Album))
CurrentAlbum = eventData.Album;
if (!string.IsNullOrEmpty(eventData.CoverArt))
CurrentAlbumArtUrl = eventData.CoverArt;
if (!string.IsNullOrEmpty(eventData.StationName))
{
CurrentStation = eventData.StationName;
StationChanged?.Invoke(this, CurrentStation);
}
// Raise event based on event type
if (!string.IsNullOrEmpty(eventData.Event))
{
switch (eventData.Event.ToLower())
{
case "songstart":
IsPlaying = true;
TriggerSongChanged();
break;
case "songfinish":
break;
case "songlove":
RaiseEvent("Song Loved");
break;
case "songban":
RaiseEvent("Song Banned");
break;
default:
RaiseEvent($"Event: {eventData.Event}");
break;
}
}
}
private void RaiseEvent(string eventText)
{
EventOccurred?.Invoke(this, new PianobarEventArgs { EventText = eventText });
@@ -287,6 +512,14 @@ namespace PainoBar_Helper
{
try
{
// Stop event monitoring
if (_eventMonitor != null)
{
_eventMonitor.EventReceived -= OnEventReceived;
_eventMonitor.Dispose();
_eventMonitor = null;
}
if (_process != null)
{
_process.OutputDataReceived -= Process_OutputDataReceived;
+81 -1
View File
@@ -45,7 +45,7 @@ namespace PainoBar_Helper
throw new ArgumentException($"Profile '{profileName}' does not exist.");
_activeProfileName = profileName;
SaveProfiles();
SaveProfiles(); // Save immediately to persist the active profile
ActiveProfileChanged?.Invoke(this, EventArgs.Empty);
}
@@ -155,6 +155,10 @@ namespace PainoBar_Helper
if (!string.IsNullOrEmpty(configDir))
Directory.CreateDirectory(configDir);
// Generate event command script path
var eventCmdPath = Path.Combine(configDir, "eventcmd.cmd");
GenerateEventCommandScript(eventCmdPath, profile);
var sb = new StringBuilder();
sb.AppendLine("# Pianobar configuration file");
sb.AppendLine($"# Generated by Pianobar Helper for profile: {profile.Name}");
@@ -169,6 +173,16 @@ namespace PainoBar_Helper
sb.AppendLine();
}
// Add event command directive
sb.AppendLine($"event_command = \"{eventCmdPath}\"");
sb.AppendLine();
// Additional settings for better integration
sb.AppendLine("# Output formatting");
sb.AppendLine("format_nowplaying_song = %t|%a|%l|%r");
sb.AppendLine("format_station_list = %i) %n");
sb.AppendLine();
File.WriteAllText(settings.ConfigFilePath, sb.ToString());
}
catch (Exception ex)
@@ -177,6 +191,53 @@ namespace PainoBar_Helper
}
}
private void GenerateEventCommandScript(string scriptPath, Profile profile)
{
try
{
// Create the directory for the event output file
var appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"PianobarHelper"
);
Directory.CreateDirectory(appDataPath);
var eventOutputPath = Path.Combine(appDataPath, $"events_{profile.Name}.json");
// Generate Windows batch script for event handling
var sb = new StringBuilder();
sb.AppendLine("@echo off");
sb.AppendLine("REM Pianobar event command script");
sb.AppendLine("REM Generated by Pianobar Helper");
sb.AppendLine();
sb.AppendLine($"set \"EVENT_OUTPUT={eventOutputPath}\"");
sb.AppendLine();
sb.AppendLine("REM Create JSON output for the event");
sb.AppendLine("(");
sb.AppendLine(" echo {");
sb.AppendLine(" echo \"event\": \"%1\",");
sb.AppendLine(" echo \"timestamp\": \"%date% %time%\",");
sb.AppendLine(" echo \"artist\": \"%artist%\",");
sb.AppendLine(" echo \"title\": \"%title%\",");
sb.AppendLine(" echo \"album\": \"%album%\",");
sb.AppendLine(" echo \"coverArt\": \"%coverArt%\",");
sb.AppendLine(" echo \"stationName\": \"%stationName%\",");
sb.AppendLine(" echo \"songDuration\": \"%songDuration%\",");
sb.AppendLine(" echo \"songPlayed\": \"%songPlayed%\",");
sb.AppendLine(" echo \"rating\": \"%rating%\",");
sb.AppendLine(" echo \"detailUrl\": \"%detailUrl%\"");
sb.AppendLine(" echo }");
sb.AppendLine(") > \"%EVENT_OUTPUT%\"");
sb.AppendLine();
sb.AppendLine("exit /b 0");
File.WriteAllText(scriptPath, sb.ToString());
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to generate event command script: {ex.Message}", ex);
}
}
public string? GetActiveConfigPath()
{
var activeProfile = GetActiveProfile();
@@ -184,6 +245,19 @@ namespace PainoBar_Helper
return activeProfile.Settings.ConfigFilePath;
return null;
}
public string? GetEventOutputPath()
{
var activeProfile = GetActiveProfile();
if (activeProfile == null)
return null;
var appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"PianobarHelper"
);
return Path.Combine(appDataPath, $"events_{activeProfile.Name}.json");
}
}
public class Profile
@@ -194,6 +268,9 @@ namespace PainoBar_Helper
public class ProfileSettings
{
// Pianobar Path
public string PianobarPath { get; set; } = "pianobar.exe";
// Output Settings
public bool EnableTextFile { get; set; }
public string TextFilePath { get; set; } = string.Empty;
@@ -209,6 +286,9 @@ namespace PainoBar_Helper
public string ConfigFilePath { get; set; } = string.Empty;
public string PianobarUser { get; set; } = string.Empty;
public string PianobarPassword { get; set; } = string.Empty;
// Process Management
public bool KillProcessOnDisconnect { get; set; }
}
internal class ProfileData
+234 -188
View File
@@ -29,36 +29,39 @@
private void InitializeComponent()
{
profiles_group_box = new GroupBox();
selected_profile_label = new Label();
profiles_combo_box_selector = new ComboBox();
new_profile_button = new Button();
delete_profile_button = new Button();
new_profile_button = new Button();
profiles_combo_box_selector = new ComboBox();
selected_profile_label = new Label();
profile_name_label = new Label();
profile_name_text_box = new TextBox();
profile_configuration_group_box = new GroupBox();
pianobar_path_label = new Label();
pianobar_path_text_box = new TextBox();
browse_button = new Button();
profile_config_tab_control = new TabControl();
outputs_tab_page = new TabPage();
pianobar_override_tab_page = new TabPage();
obs_text_output_check_box = new CheckBox();
output_label = new Label();
output_folder_text_box = new TextBox();
output_folder_browse_button = new Button();
local_web_server_check_box = new CheckBox();
port_label = new Label();
textBox1 = new TextBox();
chk_toast_notifications = new CheckBox();
chkSMTC = new CheckBox();
chkOverrideCredentials = new CheckBox();
txtConfigFilePathLabel = new Label();
txtConfigFilePath = new TextBox();
btnBrowseConfigFile = new Button();
UsernameLabel = new Label();
txtPianobarUser = new TextBox();
PasswordLabel = new Label();
chk_toast_notifications = new CheckBox();
textBox1 = new TextBox();
port_label = new Label();
local_web_server_check_box = new CheckBox();
output_folder_browse_button = new Button();
output_folder_text_box = new TextBox();
output_label = new Label();
obs_text_output_check_box = new CheckBox();
pianobar_override_tab_page = new TabPage();
txtPianobarPass = new TextBox();
PasswordLabel = new Label();
txtPianobarUser = new TextBox();
UsernameLabel = new Label();
btnBrowseConfigFile = new Button();
txtConfigFilePath = new TextBox();
txtConfigFilePathLabel = new Label();
chkOverrideCredentials = new CheckBox();
painobar_exe_browse_button = new Button();
pianobar_path_text_box = new TextBox();
pianobar_path_label = new Label();
save_button = new Button();
cancel_button = new Button();
chkkillpro = new CheckBox();
profiles_group_box.SuspendLayout();
profile_configuration_group_box.SuspendLayout();
profile_config_tab_control.SuspendLayout();
@@ -79,23 +82,15 @@
profiles_group_box.TabStop = false;
profiles_group_box.Text = "Profile Management";
//
// selected_profile_label
// delete_profile_button
//
selected_profile_label.AutoSize = true;
selected_profile_label.Location = new Point(16, 36);
selected_profile_label.Name = "selected_profile_label";
selected_profile_label.Size = new Size(102, 17);
selected_profile_label.TabIndex = 0;
selected_profile_label.Text = "Selected profile:";
//
// profiles_combo_box_selector
//
profiles_combo_box_selector.FormattingEnabled = true;
profiles_combo_box_selector.Location = new Point(124, 32);
profiles_combo_box_selector.Name = "profiles_combo_box_selector";
profiles_combo_box_selector.Size = new Size(199, 25);
profiles_combo_box_selector.TabIndex = 1;
profiles_combo_box_selector.SelectedIndexChanged += profiles_combo_box_selector_SelectedIndexChanged;
delete_profile_button.Location = new Point(329, 49);
delete_profile_button.Name = "delete_profile_button";
delete_profile_button.Size = new Size(75, 23);
delete_profile_button.TabIndex = 3;
delete_profile_button.Text = "Delete";
delete_profile_button.UseVisualStyleBackColor = true;
delete_profile_button.Click += delete_profile_button_Click;
//
// new_profile_button
//
@@ -107,15 +102,24 @@
new_profile_button.UseVisualStyleBackColor = true;
new_profile_button.Click += new_profile_button_Click;
//
// delete_profile_button
// profiles_combo_box_selector
//
delete_profile_button.Location = new Point(329, 49);
delete_profile_button.Name = "delete_profile_button";
delete_profile_button.Size = new Size(75, 23);
delete_profile_button.TabIndex = 3;
delete_profile_button.Text = "Delete";
delete_profile_button.UseVisualStyleBackColor = true;
delete_profile_button.Click += delete_profile_button_Click;
profiles_combo_box_selector.FormattingEnabled = true;
profiles_combo_box_selector.Location = new Point(124, 32);
profiles_combo_box_selector.Name = "profiles_combo_box_selector";
profiles_combo_box_selector.Size = new Size(199, 25);
profiles_combo_box_selector.TabIndex = 1;
profiles_combo_box_selector.SelectedIndexChanged += profiles_combo_box_selector_SelectedIndexChanged;
profiles_combo_box_selector.TextChanged += profiles_combo_box_selector_TextChanged;
//
// selected_profile_label
//
selected_profile_label.AutoSize = true;
selected_profile_label.Location = new Point(16, 36);
selected_profile_label.Name = "selected_profile_label";
selected_profile_label.Size = new Size(102, 17);
selected_profile_label.TabIndex = 0;
selected_profile_label.Text = "Selected profile:";
//
// profile_name_label
//
@@ -136,7 +140,7 @@
// profile_configuration_group_box
//
profile_configuration_group_box.Controls.Add(profile_config_tab_control);
profile_configuration_group_box.Controls.Add(browse_button);
profile_configuration_group_box.Controls.Add(painobar_exe_browse_button);
profile_configuration_group_box.Controls.Add(pianobar_path_text_box);
profile_configuration_group_box.Controls.Add(pianobar_path_label);
profile_configuration_group_box.Controls.Add(profile_name_text_box);
@@ -148,31 +152,6 @@
profile_configuration_group_box.TabStop = false;
profile_configuration_group_box.Text = "Profile Configuration";
//
// pianobar_path_label
//
pianobar_path_label.AutoSize = true;
pianobar_path_label.Location = new Point(12, 55);
pianobar_path_label.Name = "pianobar_path_label";
pianobar_path_label.Size = new Size(86, 17);
pianobar_path_label.TabIndex = 2;
pianobar_path_label.Text = "Pianobar Path:";
//
// pianobar_path_text_box
//
pianobar_path_text_box.Location = new Point(104, 53);
pianobar_path_text_box.Name = "pianobar_path_text_box";
pianobar_path_text_box.Size = new Size(224, 24);
pianobar_path_text_box.TabIndex = 3;
//
// browse_button
//
browse_button.Location = new Point(334, 53);
browse_button.Name = "browse_button";
browse_button.Size = new Size(75, 23);
browse_button.TabIndex = 4;
browse_button.Text = "Browse";
browse_button.UseVisualStyleBackColor = true;
//
// profile_config_tab_control
//
profile_config_tab_control.Controls.Add(outputs_tab_page);
@@ -202,8 +181,91 @@
outputs_tab_page.Text = "Outputs";
outputs_tab_page.UseVisualStyleBackColor = true;
//
// chkSMTC
//
chkSMTC.AutoSize = true;
chkSMTC.Location = new Point(18, 198);
chkSMTC.Name = "chkSMTC";
chkSMTC.Size = new Size(251, 21);
chkSMTC.TabIndex = 8;
chkSMTC.Text = "Windows System Media Controls (SMTC)";
chkSMTC.UseVisualStyleBackColor = true;
//
// chk_toast_notifications
//
chk_toast_notifications.AutoSize = true;
chk_toast_notifications.Location = new Point(18, 149);
chk_toast_notifications.Name = "chk_toast_notifications";
chk_toast_notifications.Size = new Size(223, 21);
chk_toast_notifications.TabIndex = 7;
chk_toast_notifications.Text = "Show Windows Toast Notifications";
chk_toast_notifications.UseVisualStyleBackColor = true;
//
// textBox1
//
textBox1.Location = new Point(111, 105);
textBox1.Name = "textBox1";
textBox1.Size = new Size(169, 24);
textBox1.TabIndex = 6;
//
// port_label
//
port_label.AutoSize = true;
port_label.Location = new Point(18, 110);
port_label.Name = "port_label";
port_label.Size = new Size(36, 17);
port_label.TabIndex = 5;
port_label.Text = "Port:";
//
// local_web_server_check_box
//
local_web_server_check_box.AutoSize = true;
local_web_server_check_box.Location = new Point(18, 77);
local_web_server_check_box.Name = "local_web_server_check_box";
local_web_server_check_box.Size = new Size(166, 21);
local_web_server_check_box.TabIndex = 4;
local_web_server_check_box.Text = "Enable Local Web Server";
local_web_server_check_box.UseVisualStyleBackColor = true;
//
// output_folder_browse_button
//
output_folder_browse_button.Location = new Point(288, 45);
output_folder_browse_button.Name = "output_folder_browse_button";
output_folder_browse_button.Size = new Size(75, 23);
output_folder_browse_button.TabIndex = 3;
output_folder_browse_button.Text = "Browse";
output_folder_browse_button.UseVisualStyleBackColor = true;
output_folder_browse_button.Click += output_folder_browse_button_Click;
//
// output_folder_text_box
//
output_folder_text_box.Location = new Point(111, 43);
output_folder_text_box.Name = "output_folder_text_box";
output_folder_text_box.Size = new Size(169, 24);
output_folder_text_box.TabIndex = 2;
//
// output_label
//
output_label.AutoSize = true;
output_label.Location = new Point(18, 47);
output_label.Name = "output_label";
output_label.Size = new Size(91, 17);
output_label.TabIndex = 1;
output_label.Text = "Output Folder:";
//
// obs_text_output_check_box
//
obs_text_output_check_box.AutoSize = true;
obs_text_output_check_box.Location = new Point(18, 18);
obs_text_output_check_box.Name = "obs_text_output_check_box";
obs_text_output_check_box.Size = new Size(219, 21);
obs_text_output_check_box.TabIndex = 0;
obs_text_output_check_box.Text = "File output (For OBS / Streaming)";
obs_text_output_check_box.UseVisualStyleBackColor = true;
//
// pianobar_override_tab_page
//
pianobar_override_tab_page.Controls.Add(chkkillpro);
pianobar_override_tab_page.Controls.Add(txtPianobarPass);
pianobar_override_tab_page.Controls.Add(PasswordLabel);
pianobar_override_tab_page.Controls.Add(txtPianobarUser);
@@ -220,87 +282,68 @@
pianobar_override_tab_page.Text = "Pianobar Config & Auth";
pianobar_override_tab_page.UseVisualStyleBackColor = true;
//
// obs_text_output_check_box
// txtPianobarPass
//
obs_text_output_check_box.AutoSize = true;
obs_text_output_check_box.Location = new Point(18, 18);
obs_text_output_check_box.Name = "obs_text_output_check_box";
obs_text_output_check_box.Size = new Size(219, 21);
obs_text_output_check_box.TabIndex = 0;
obs_text_output_check_box.Text = "File output (For OBS / Streaming)";
obs_text_output_check_box.UseVisualStyleBackColor = true;
txtPianobarPass.Enabled = false;
txtPianobarPass.Location = new Point(133, 109);
txtPianobarPass.Name = "txtPianobarPass";
txtPianobarPass.Size = new Size(152, 24);
txtPianobarPass.TabIndex = 7;
txtPianobarPass.UseSystemPasswordChar = true;
//
// output_label
// PasswordLabel
//
output_label.AutoSize = true;
output_label.Location = new Point(18, 47);
output_label.Name = "output_label";
output_label.Size = new Size(91, 17);
output_label.TabIndex = 1;
output_label.Text = "Output Folder:";
PasswordLabel.AutoSize = true;
PasswordLabel.Location = new Point(19, 115);
PasswordLabel.Name = "PasswordLabel";
PasswordLabel.Size = new Size(62, 17);
PasswordLabel.TabIndex = 6;
PasswordLabel.Text = "Password:";
//
// output_folder_text_box
// txtPianobarUser
//
output_folder_text_box.Location = new Point(111, 43);
output_folder_text_box.Name = "output_folder_text_box";
output_folder_text_box.Size = new Size(169, 24);
output_folder_text_box.TabIndex = 2;
txtPianobarUser.Enabled = false;
txtPianobarUser.Location = new Point(134, 76);
txtPianobarUser.Name = "txtPianobarUser";
txtPianobarUser.Size = new Size(152, 24);
txtPianobarUser.TabIndex = 5;
//
// output_folder_browse_button
// UsernameLabel
//
output_folder_browse_button.Location = new Point(288, 45);
output_folder_browse_button.Name = "output_folder_browse_button";
output_folder_browse_button.Size = new Size(75, 23);
output_folder_browse_button.TabIndex = 3;
output_folder_browse_button.Text = "Browse";
output_folder_browse_button.UseVisualStyleBackColor = true;
output_folder_browse_button.Click += output_folder_browse_button_Click;
UsernameLabel.AutoSize = true;
UsernameLabel.Location = new Point(19, 79);
UsernameLabel.Name = "UsernameLabel";
UsernameLabel.Size = new Size(109, 17);
UsernameLabel.TabIndex = 4;
UsernameLabel.Text = "Username / Email:";
//
// local_web_server_check_box
// btnBrowseConfigFile
//
local_web_server_check_box.AutoSize = true;
local_web_server_check_box.Location = new Point(18, 77);
local_web_server_check_box.Name = "local_web_server_check_box";
local_web_server_check_box.Size = new Size(166, 21);
local_web_server_check_box.TabIndex = 4;
local_web_server_check_box.Text = "Enable Local Web Server";
local_web_server_check_box.UseVisualStyleBackColor = true;
btnBrowseConfigFile.Enabled = false;
btnBrowseConfigFile.Location = new Point(292, 43);
btnBrowseConfigFile.Name = "btnBrowseConfigFile";
btnBrowseConfigFile.Size = new Size(75, 23);
btnBrowseConfigFile.TabIndex = 3;
btnBrowseConfigFile.Text = "Browse";
btnBrowseConfigFile.UseVisualStyleBackColor = true;
btnBrowseConfigFile.Click += btnBrowseConfigFile_Click;
//
// port_label
// txtConfigFilePath
//
port_label.AutoSize = true;
port_label.Location = new Point(18, 110);
port_label.Name = "port_label";
port_label.Size = new Size(36, 17);
port_label.TabIndex = 5;
port_label.Text = "Port:";
txtConfigFilePath.Enabled = false;
txtConfigFilePath.Location = new Point(121, 42);
txtConfigFilePath.Name = "txtConfigFilePath";
txtConfigFilePath.Size = new Size(165, 24);
txtConfigFilePath.TabIndex = 2;
//
// textBox1
// txtConfigFilePathLabel
//
textBox1.Location = new Point(111, 105);
textBox1.Name = "textBox1";
textBox1.Size = new Size(169, 24);
textBox1.TabIndex = 6;
//
// chk_toast_notifications
//
chk_toast_notifications.AutoSize = true;
chk_toast_notifications.Location = new Point(18, 149);
chk_toast_notifications.Name = "chk_toast_notifications";
chk_toast_notifications.Size = new Size(223, 21);
chk_toast_notifications.TabIndex = 7;
chk_toast_notifications.Text = "Show Windows Toast Notifications";
chk_toast_notifications.UseVisualStyleBackColor = true;
//
// chkSMTC
//
chkSMTC.AutoSize = true;
chkSMTC.Location = new Point(18, 198);
chkSMTC.Name = "chkSMTC";
chkSMTC.Size = new Size(251, 21);
chkSMTC.TabIndex = 8;
chkSMTC.Text = "Windows System Media Controls (SMTC)";
chkSMTC.UseVisualStyleBackColor = true;
txtConfigFilePathLabel.AutoSize = true;
txtConfigFilePathLabel.Location = new Point(14, 44);
txtConfigFilePathLabel.Name = "txtConfigFilePathLabel";
txtConfigFilePathLabel.Size = new Size(105, 17);
txtConfigFilePathLabel.TabIndex = 1;
txtConfigFilePathLabel.Text = "Config Save Path:";
//
// chkOverrideCredentials
//
@@ -313,70 +356,70 @@
chkOverrideCredentials.UseVisualStyleBackColor = true;
chkOverrideCredentials.CheckedChanged += chkOverrideCredentials_CheckedChanged;
//
// txtConfigFilePathLabel
// painobar_exe_browse_button
//
txtConfigFilePathLabel.AutoSize = true;
txtConfigFilePathLabel.Location = new Point(14, 44);
txtConfigFilePathLabel.Name = "txtConfigFilePathLabel";
txtConfigFilePathLabel.Size = new Size(105, 17);
txtConfigFilePathLabel.TabIndex = 1;
txtConfigFilePathLabel.Text = "Config Save Path:";
painobar_exe_browse_button.Location = new Point(334, 53);
painobar_exe_browse_button.Name = "painobar_exe_browse_button";
painobar_exe_browse_button.Size = new Size(75, 23);
painobar_exe_browse_button.TabIndex = 4;
painobar_exe_browse_button.Text = "Browse";
painobar_exe_browse_button.UseVisualStyleBackColor = true;
painobar_exe_browse_button.Click += painobar_exe_browse_button_Click;
//
// txtConfigFilePath
// pianobar_path_text_box
//
txtConfigFilePath.Location = new Point(121, 42);
txtConfigFilePath.Name = "txtConfigFilePath";
txtConfigFilePath.Size = new Size(165, 24);
txtConfigFilePath.TabIndex = 2;
pianobar_path_text_box.Location = new Point(104, 53);
pianobar_path_text_box.Name = "pianobar_path_text_box";
pianobar_path_text_box.Size = new Size(224, 24);
pianobar_path_text_box.TabIndex = 3;
//
// btnBrowseConfigFile
// pianobar_path_label
//
btnBrowseConfigFile.Location = new Point(292, 43);
btnBrowseConfigFile.Name = "btnBrowseConfigFile";
btnBrowseConfigFile.Size = new Size(75, 23);
btnBrowseConfigFile.TabIndex = 3;
btnBrowseConfigFile.Text = "Browse";
btnBrowseConfigFile.UseVisualStyleBackColor = true;
btnBrowseConfigFile.Click += btnBrowseConfigFile_Click;
pianobar_path_label.AutoSize = true;
pianobar_path_label.Location = new Point(12, 55);
pianobar_path_label.Name = "pianobar_path_label";
pianobar_path_label.Size = new Size(86, 17);
pianobar_path_label.TabIndex = 2;
pianobar_path_label.Text = "Pianobar Path:";
//
// UsernameLabel
// save_button
//
UsernameLabel.AutoSize = true;
UsernameLabel.Location = new Point(19, 79);
UsernameLabel.Name = "UsernameLabel";
UsernameLabel.Size = new Size(109, 17);
UsernameLabel.TabIndex = 4;
UsernameLabel.Text = "Username / Email:";
save_button.Location = new Point(66, 551);
save_button.Name = "save_button";
save_button.Size = new Size(75, 23);
save_button.TabIndex = 2;
save_button.Text = "Save";
save_button.UseVisualStyleBackColor = true;
save_button.Click += btnSave_Click;
//
// txtPianobarUser
// cancel_button
//
txtPianobarUser.Location = new Point(134, 76);
txtPianobarUser.Name = "txtPianobarUser";
txtPianobarUser.Size = new Size(152, 24);
txtPianobarUser.TabIndex = 5;
cancel_button.Location = new Point(303, 551);
cancel_button.Name = "cancel_button";
cancel_button.Size = new Size(75, 23);
cancel_button.TabIndex = 3;
cancel_button.Text = "Cancel";
cancel_button.UseVisualStyleBackColor = true;
cancel_button.Click += btnCancel_Click;
//
// PasswordLabel
// chkkillpro
//
PasswordLabel.AutoSize = true;
PasswordLabel.Location = new Point(19, 115);
PasswordLabel.Name = "PasswordLabel";
PasswordLabel.Size = new Size(62, 17);
PasswordLabel.TabIndex = 6;
PasswordLabel.Text = "Password:";
//
// txtPianobarPass
//
txtPianobarPass.Location = new Point(133, 109);
txtPianobarPass.Name = "txtPianobarPass";
txtPianobarPass.Size = new Size(152, 24);
txtPianobarPass.TabIndex = 7;
txtPianobarPass.UseSystemPasswordChar = true;
chkkillpro.AutoSize = true;
chkkillpro.Enabled = false;
chkkillpro.Location = new Point(262, 150);
chkkillpro.Name = "chkkillpro";
chkkillpro.Size = new Size(92, 21);
chkkillpro.TabIndex = 8;
chkkillpro.Text = "Kill process";
chkkillpro.UseVisualStyleBackColor = true;
//
// SettingsForm
//
AutoScaleDimensions = new SizeF(7F, 17F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(454, 586);
Controls.Add(cancel_button);
Controls.Add(save_button);
Controls.Add(profile_configuration_group_box);
Controls.Add(profiles_group_box);
Name = "SettingsForm";
@@ -403,7 +446,7 @@
private Label profile_name_label;
private TextBox profile_name_text_box;
private GroupBox profile_configuration_group_box;
private Button browse_button;
private Button painobar_exe_browse_button;
private TextBox pianobar_path_text_box;
private Label pianobar_path_label;
private TabControl profile_config_tab_control;
@@ -426,5 +469,8 @@
private Label UsernameLabel;
private Button btnBrowseConfigFile;
private TextBox txtPianobarPass;
private Button save_button;
private Button cancel_button;
private CheckBox chkkillpro;
}
}
+140 -12
View File
@@ -13,6 +13,7 @@ namespace PainoBar_Helper
_profileManager = profileManager;
_currentSettings = new ProfileSettings
{
PianobarPath = currentSettings.PianobarPath,
EnableTextFile = currentSettings.EnableTextFile,
TextFilePath = currentSettings.TextFilePath,
EnableWebServer = currentSettings.EnableWebServer,
@@ -22,7 +23,8 @@ namespace PainoBar_Helper
OverrideCredentials = currentSettings.OverrideCredentials,
ConfigFilePath = currentSettings.ConfigFilePath,
PianobarUser = currentSettings.PianobarUser,
PianobarPassword = currentSettings.PianobarPassword
PianobarPassword = currentSettings.PianobarPassword,
KillProcessOnDisconnect = currentSettings.KillProcessOnDisconnect
};
_currentProfileName = profileManager.ActiveProfileName;
@@ -43,6 +45,8 @@ namespace PainoBar_Helper
if (profiles_combo_box_selector == null)
return;
string? currentText = profiles_combo_box_selector.Text;
profiles_combo_box_selector.Items.Clear();
foreach (var profile in _profileManager.Profiles)
{
@@ -51,10 +55,20 @@ namespace PainoBar_Helper
if (!string.IsNullOrEmpty(_currentProfileName))
profiles_combo_box_selector.SelectedItem = _currentProfileName;
else if (!string.IsNullOrEmpty(currentText))
profiles_combo_box_selector.Text = currentText;
}
private void LoadSettingsToControls()
{
// Update profile name text box with current profile name
if (profile_name_text_box != null && !string.IsNullOrEmpty(_currentProfileName))
profile_name_text_box.Text = _currentProfileName;
// Pianobar executable path
if (pianobar_path_text_box != null)
pianobar_path_text_box.Text = _currentSettings.PianobarPath;
// Output Settings (Tab 1)
if (obs_text_output_check_box != null)
obs_text_output_check_box.Checked = _currentSettings.EnableTextFile;
@@ -89,10 +103,17 @@ namespace PainoBar_Helper
if (txtPianobarPass != null)
txtPianobarPass.Text = _currentSettings.PianobarPassword;
if (chkkillpro != null)
chkkillpro.Checked = _currentSettings.KillProcessOnDisconnect;
}
private void SaveSettingsFromControls()
{
// Pianobar Path
if (pianobar_path_text_box != null)
_currentSettings.PianobarPath = pianobar_path_text_box.Text;
// Output Settings
if (obs_text_output_check_box != null)
_currentSettings.EnableTextFile = obs_text_output_check_box.Checked;
@@ -124,6 +145,9 @@ namespace PainoBar_Helper
if (txtPianobarPass != null)
_currentSettings.PianobarPassword = txtPianobarPass.Text;
if (chkkillpro != null)
_currentSettings.KillProcessOnDisconnect = chkkillpro.Checked;
}
#endregion
@@ -133,15 +157,23 @@ namespace PainoBar_Helper
// Wire this to profiles_combo_box_selector.SelectedIndexChanged in the Designer
public void profiles_combo_box_selector_SelectedIndexChanged(object? sender, EventArgs e)
{
if (sender is not ComboBox cmb || cmb.SelectedItem is not string profileName)
if (sender is not ComboBox cmb)
return;
// Update profile name text box when selection changes
if (profile_name_text_box != null)
profile_name_text_box.Text = cmb.Text;
// If selecting an existing profile, load its settings
if (cmb.SelectedItem is string profileName)
{
var profile = _profileManager.Profiles.FirstOrDefault(p => p.Name == profileName);
if (profile != null)
{
_currentProfileName = profileName;
_currentSettings = new ProfileSettings
{
PianobarPath = profile.Settings.PianobarPath,
EnableTextFile = profile.Settings.EnableTextFile,
TextFilePath = profile.Settings.TextFilePath,
EnableWebServer = profile.Settings.EnableWebServer,
@@ -151,28 +183,60 @@ namespace PainoBar_Helper
OverrideCredentials = profile.Settings.OverrideCredentials,
ConfigFilePath = profile.Settings.ConfigFilePath,
PianobarUser = profile.Settings.PianobarUser,
PianobarPassword = profile.Settings.PianobarPassword
PianobarPassword = profile.Settings.PianobarPassword,
KillProcessOnDisconnect = profile.Settings.KillProcessOnDisconnect
};
LoadSettingsToControls();
}
}
}
// Wire this to profiles_combo_box_selector.TextChanged in the Designer
public void profiles_combo_box_selector_TextChanged(object? sender, EventArgs e)
{
if (sender is not ComboBox cmb)
return;
// Keep profile name text box in sync with combo box text
if (profile_name_text_box != null)
profile_name_text_box.Text = cmb.Text;
}
// Wire this to new_profile_button.Click in the Designer
public void new_profile_button_Click(object? sender, EventArgs e)
{
string? profileName = PromptForProfileName("New Profile", "Enter profile name:");
// Get the profile name from the combo box text
string? profileName = profiles_combo_box_selector?.Text?.Trim();
if (string.IsNullOrWhiteSpace(profileName))
{
MessageBox.Show("Please enter a profile name in the dropdown box.", "No Profile Name",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// Check if profile already exists
if (_profileManager.Profiles.Any(p => p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase)))
{
MessageBox.Show($"Profile '{profileName}' already exists. Please choose a different name.", "Profile Exists",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
var newSettings = new ProfileSettings();
_profileManager.CreateProfile(profileName, newSettings);
// Create new profile with current settings
SaveSettingsFromControls();
_profileManager.CreateProfile(profileName, _currentSettings);
_currentProfileName = profileName;
LoadProfileList();
if (profiles_combo_box_selector != null)
profiles_combo_box_selector.SelectedItem = profileName;
if (profile_name_text_box != null)
profile_name_text_box.Text = profileName;
MessageBox.Show($"Profile '{profileName}' created successfully.", "Profile Created",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
@@ -186,20 +250,32 @@ namespace PainoBar_Helper
// Wire this to delete_profile_button.Click in the Designer
public void delete_profile_button_Click(object? sender, EventArgs e)
{
if (string.IsNullOrEmpty(_currentProfileName))
// Get the profile name from either the combo box or the profile name text box
string? profileName = profiles_combo_box_selector?.Text?.Trim();
if (string.IsNullOrWhiteSpace(profileName))
{
MessageBox.Show("No profile selected.", "No Selection", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var result = MessageBox.Show($"Are you sure you want to delete profile '{_currentProfileName}'?",
// Check if profile exists
if (!_profileManager.Profiles.Any(p => p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase)))
{
MessageBox.Show($"Profile '{profileName}' does not exist.", "Profile Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var result = MessageBox.Show($"Are you sure you want to delete profile '{profileName}'?",
"Confirm Deletion", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
try
{
_profileManager.DeleteProfile(_currentProfileName);
_profileManager.DeleteProfile(profileName);
// Reset to active profile or clear
_currentProfileName = _profileManager.ActiveProfileName;
LoadProfileList();
@@ -209,8 +285,18 @@ namespace PainoBar_Helper
_currentSettings = activeProfile.Settings;
LoadSettingsToControls();
}
else
{
// No profiles left, clear the form
_currentSettings = new ProfileSettings();
LoadSettingsToControls();
if (profiles_combo_box_selector != null)
profiles_combo_box_selector.Text = string.Empty;
if (profile_name_text_box != null)
profile_name_text_box.Text = string.Empty;
}
MessageBox.Show("Profile deleted successfully.", "Profile Deleted",
MessageBox.Show($"Profile '{profileName}' deleted successfully.", "Profile Deleted",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
@@ -225,6 +311,24 @@ namespace PainoBar_Helper
#region File Browsing
// Wire this to painobar_exe_browse_button.Click in the Designer
public void painobar_exe_browse_button_Click(object? sender, EventArgs e)
{
using var dialog = new OpenFileDialog
{
Filter = "Executable Files (*.exe)|*.exe|All Files (*.*)|*.*",
Title = "Select Pianobar Executable",
FileName = "pianobar.exe",
CheckFileExists = true
};
if (dialog.ShowDialog() == DialogResult.OK)
{
if (pianobar_path_text_box != null)
pianobar_path_text_box.Text = dialog.FileName;
}
}
// Wire this to output_folder_browse_button.Click in the Designer
public void output_folder_browse_button_Click(object? sender, EventArgs e)
{
@@ -283,6 +387,9 @@ namespace PainoBar_Helper
if (txtPianobarPass != null)
txtPianobarPass.Enabled = enabled;
if (chkkillpro != null)
chkkillpro.Enabled = enabled;
}
#endregion
@@ -296,11 +403,32 @@ namespace PainoBar_Helper
{
SaveSettingsFromControls();
if (!string.IsNullOrEmpty(_currentProfileName))
// Get the profile name from the text box (or combo box as fallback)
string? profileName = profile_name_text_box?.Text?.Trim();
if (string.IsNullOrEmpty(profileName))
profileName = profiles_combo_box_selector?.Text?.Trim();
if (string.IsNullOrWhiteSpace(profileName))
{
_profileManager.UpdateProfile(_currentProfileName, _currentSettings);
MessageBox.Show("Please enter a profile name before saving.", "No Profile Name",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// Update existing profile or create new one
if (_profileManager.Profiles.Any(p => p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase)))
{
_profileManager.UpdateProfile(profileName, _currentSettings);
}
else
{
_profileManager.CreateProfile(profileName, _currentSettings);
}
// Set this profile as the active profile
_profileManager.SetActiveProfile(profileName);
_currentProfileName = profileName;
DialogResult = DialogResult.OK;
Close();
}
+16 -16
View File
@@ -29,9 +29,9 @@
private void InitializeComponent()
{
station_list_group_box = new GroupBox();
search_label = new Label();
textBox1 = new TextBox();
lstStations = new ListBox();
textBox1 = new TextBox();
search_label = new Label();
refresh_button = new Button();
btnSelectStation = new Button();
btnCancelStation = new Button();
@@ -50,14 +50,13 @@
station_list_group_box.TabStop = false;
station_list_group_box.Text = "Available Stations";
//
// search_label
// lstStations
//
search_label.AutoSize = true;
search_label.Location = new Point(26, 32);
search_label.Name = "search_label";
search_label.Size = new Size(50, 17);
search_label.TabIndex = 0;
search_label.Text = "Search:";
lstStations.FormattingEnabled = true;
lstStations.Location = new Point(26, 65);
lstStations.Name = "lstStations";
lstStations.Size = new Size(332, 174);
lstStations.TabIndex = 2;
//
// textBox1
//
@@ -67,13 +66,14 @@
textBox1.TabIndex = 1;
textBox1.TextChanged += textBox1_TextChanged;
//
// lstStations
// search_label
//
lstStations.FormattingEnabled = true;
lstStations.Location = new Point(26, 65);
lstStations.Name = "lstStations";
lstStations.Size = new Size(332, 174);
lstStations.TabIndex = 2;
search_label.AutoSize = true;
search_label.Location = new Point(26, 32);
search_label.Name = "search_label";
search_label.Size = new Size(50, 17);
search_label.TabIndex = 0;
search_label.Text = "Search:";
//
// refresh_button
//
@@ -90,7 +90,7 @@
btnSelectStation.Name = "btnSelectStation";
btnSelectStation.Size = new Size(117, 23);
btnSelectStation.TabIndex = 2;
btnSelectStation.Text = "Refresh stations";
btnSelectStation.Text = "Select Station";
btnSelectStation.UseVisualStyleBackColor = true;
btnSelectStation.Click += btnSelectStation_Click;
//
+33 -7
View File
@@ -6,6 +6,7 @@ namespace PainoBar_Helper
private List<string> _stations;
private List<string> _filteredStations;
private bool _stationsLoaded = false;
private bool _stationEntriesStarted = false;
public StationForm(PianobarManager pianobarManager)
{
@@ -16,7 +17,6 @@ namespace PainoBar_Helper
_filteredStations = new List<string>();
InitializeControls();
LoadStations();
}
#region Initialization
@@ -32,8 +32,21 @@ namespace PainoBar_Helper
}
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
if (!_stationsLoaded)
LoadStations();
}
private void LoadStations()
{
_stations.Clear();
_filteredStations.Clear();
_stationsLoaded = false;
_stationEntriesStarted = false;
// Subscribe to output to capture station list
_pianobarManager.OutputReceived += PianobarManager_OutputReceived;
@@ -62,28 +75,41 @@ namespace PainoBar_Helper
private void PianobarManager_OutputReceived(object? sender, string e)
{
var line = StripAnsiCodes(e).Trim();
if (string.IsNullOrEmpty(line))
return;
// Parse station list from Pianobar output
// Format is typically: "0) Station Name"
var match = System.Text.RegularExpressions.Regex.Match(e, @"^\s*(\d+)\)\s*(.+)$");
// Handles formats like: "0) Station", "*0) Station", " 12) Station"
var match = System.Text.RegularExpressions.Regex.Match(line, @"^\s*\*?\s*(\d+)\)\s*(.+)$");
if (match.Success)
{
_stationEntriesStarted = true;
string stationName = match.Groups[2].Value.Trim();
if (!_stations.Contains(stationName))
{
_stations.Add(stationName);
Invoke(() => UpdateStationList());
if (!IsDisposed && IsHandleCreated)
BeginInvoke(() => UpdateStationList());
}
return;
}
// Detect end of station list
if (e.Contains("Select station:") || e.Contains("[?]"))
// Detect end of station list only after we started receiving entries
if (_stationEntriesStarted && line.Contains("Select station:", StringComparison.OrdinalIgnoreCase))
{
_stationsLoaded = true;
_pianobarManager.OutputReceived -= PianobarManager_OutputReceived;
Invoke(() => FinalizeStationList());
if (!IsDisposed && IsHandleCreated)
BeginInvoke(() => FinalizeStationList());
}
}
private static string StripAnsiCodes(string value)
{
return System.Text.RegularExpressions.Regex.Replace(value, @"\x1B\[[0-9;]*[A-Za-z]", string.Empty);
}
private void UpdateStationList()
{
if (lstStations == null)
+153
View File
@@ -0,0 +1,153 @@
# Testing Guide - Kill Process on Disconnect
## Quick Test Steps
### Test 1: Verify Initial State
1. **Run the application**
2. **Open Settings** (File ? Settings)
3. **Navigate to "Pianobar Config & Auth" tab**
4. **Verify**:
- [ ] "Enable Profile Credentials and Custom Config Generation" checkbox is **unchecked**
- [ ] "Kill process" checkbox is **disabled** (grayed out, cannot be clicked)
- [ ] Config file path field is **disabled**
- [ ] Browse button is **disabled**
- [ ] Username field is **disabled**
- [ ] Password field is **disabled**
### Test 2: Enable Override and Kill Process
1. **Check** "Enable Profile Credentials and Custom Config Generation"
2. **Verify**:
- [ ] "Kill process" checkbox becomes **enabled**
- [ ] All credential fields become **enabled**
3. **Check** "Kill process" checkbox
4. **Click Save**
5. **Re-open Settings**
6. **Verify**:
- [ ] "Enable Profile Credentials..." remains **checked**
- [ ] "Kill process" checkbox remains **checked**
- [ ] Setting persists correctly
### Test 3: Connect and Kill on Disconnect
1. **Ensure a profile with Kill Process enabled is active**
2. **Set Pianobar executable path** if not already set
3. **Click Connect**
4. **Verify**:
- [ ] Pianobar starts (status shows "Connected to Pianobar")
- [ ] PID is displayed in status bar (e.g., "PID: 12345")
5. **Click Disconnect**
6. **Verify**:
- [ ] Status shows "Pianobar process killed (PID: XXXX)"
- [ ] Pianobar process is **terminated** (check Task Manager)
### Test 4: Connect and Detach on Disconnect
1. **Open Settings**
2. **Navigate to "Pianobar Config & Auth" tab**
3. **Verify** "Enable Profile Credentials..." is **checked**
4. **Uncheck** "Kill process" checkbox
5. **Click Save**
6. **Click Connect**
7. **Verify**:
- [ ] Pianobar starts successfully
8. **Click Disconnect**
9. **Verify**:
- [ ] Status shows "Disconnected from Pianobar (process detached)"
- [ ] Pianobar process is **still running** in Task Manager
- [ ] Music continues playing
### Test 5: Disable Override (Gating)
1. **Open Settings**
2. **Navigate to "Pianobar Config & Auth" tab**
3. **Uncheck** "Enable Profile Credentials and Custom Config Generation"
4. **Verify**:
- [ ] "Kill process" checkbox becomes **disabled** (grayed out)
- [ ] All credential fields become **disabled**
- [ ] Cannot check "Kill process"
5. **Try to check "Kill process"** (should not be possible)
6. **Click Save**
7. **Close Settings**
### Test 6: Output Folder Browse Button
1. **Open Settings**
2. **Navigate to "Outputs" tab**
3. **Click** the **Browse** button next to the output folder text box (ID: `output_folder_browse_button`)
4. **Verify**:
- [ ] SaveFileDialog opens with title "Select Text File Output Path"
- [ ] Default filename is "now_playing.txt"
- [ ] Can select a location
5. **Select a location** and click OK
6. **Verify**:
- [ ] Selected path appears in the output folder text box
7. **Click Save**
8. **Verify**:
- [ ] Output path is saved to the profile
## Edge Case Tests
### Edge 1: Switch Profiles with Different Settings
1. **Create two profiles**:
- Profile A: Kill process **enabled**
- Profile B: Kill process **disabled**
2. **Switch to Profile A** and connect
3. **Disconnect** ? Should **kill** the process
4. **Switch to Profile B** and connect
5. **Disconnect** ? Should **detach** (not kill)
### Edge 2: Override Disabled Mid-Session
1. **Have Kill process enabled**
2. **Open Settings** while connected
3. **Uncheck** "Enable Profile Credentials..."
4. **Verify** "Kill process" becomes **disabled**
5. **Click Save**
6. **Disconnect**
7. **Verify** process is **detached** (not killed), since override was disabled
### Edge 3: No Active Profile
1. **Delete all profiles** or ensure no profile is active
2. **Try to connect**
3. **Verify**:
- [ ] Error message: "No active profile selected..."
- [ ] Cannot connect without a profile
## Expected Behavior Summary
| Override Enabled? | Kill Process Checked? | Disconnect Behavior |
|-------------------|----------------------|---------------------|
| ? No | N/A (disabled) | Detach |
| ? Yes | ? No | Detach |
| ? Yes | ? Yes | **Kill by PID** |
## Status Bar Messages
- **Connect starting**: "Starting Pianobar..."
- **Connect success**: "Connected to Pianobar (PID: XXXX)"
- **Connect failed**: "Failed to start Pianobar"
- **Disconnect (kill)**: "Pianobar process killed (PID: XXXX)"
- **Disconnect (detach)**: "Disconnected from Pianobar (process detached)"
## Troubleshooting
### Issue: Kill checkbox is always disabled
**Solution**: Make sure "Enable Profile Credentials and Custom Config Generation" is **checked**
### Issue: Setting doesn't persist
**Solution**: Make sure you clicked **Save** before closing the Settings dialog
### Issue: Process not killed on disconnect
**Checks**:
- Is "Enable Profile Credentials..." checked?
- Is "Kill process" checkbox checked?
- Did you save the profile?
- Is the correct profile active?
### Issue: Cannot connect
**Checks**:
- Is a profile selected and active?
- Is the Pianobar executable path set correctly?
- Does the Pianobar.exe file exist at that path?
## Files to Monitor
- `profiles.json` in `%APPDATA%\PianoBarHelper\`
- Check that `"KillProcessOnDisconnect": true` is saved for profiles where it's enabled
- Task Manager ? Details
- Watch for `pianobar.exe` process appearing/disappearing during connect/disconnect