first test

This commit is contained in:
minster586
2026-08-10 03:41:48 -04:00
parent 4c6eae510e
commit 4cefd8fb7d
10 changed files with 2022 additions and 26 deletions
+261
View File
@@ -0,0 +1,261 @@
# Pianobar Helper - Implementation Guide
## Overview
This document provides complete implementation details for the Pianobar Helper Windows Forms application.
## Architecture
### Core Components Created
1. **ProfileManager.cs** - Backend profile management system
- Handles profile creation, deletion, and configuration
- Stores profiles in JSON format in AppData
- Generates Pianobar config files with credentials
2. **PianobarManager.cs** - Process lifecycle manager
- Direct `pianobar.exe` process control
- Real-time stdout/stdin communication
- Song metadata parsing
- Event-driven architecture
3. **OutputManager.cs** - Multi-output mechanism handler
- Text file export
- Local web server (JSON endpoint)
- Windows Toast Notifications (placeholder)
- Windows SMTC integration (placeholder)
4. **MainForm (Main.cs)** - Primary application interface
5. **SettingsForm (Settings.cs)** - Profile and configuration management
6. **StationForm (StationForm.cs)** - Station picker interface
## Designer File Issues
The Designer files (.Designer.cs) need to be regenerated through Visual Studio's Form Designer. Here's what needs to be done:
### MainForm Designer Controls Required
Open `Main.cs` in the Form Designer and add these controls:
**MenuStrip (menuStrip1):**
- `fileToolStripMenuItem` - "File" menu
- `settingsToolStripMenuItem` - "Settings..." menu item
- `profilesToolStripMenuItem` - "Profiles" submenu (dynamically populated)
**StatusStrip (statusStrip1):**
- `statusLabelLeft` (ToolStripStatusLabel) - Connection indicator
- `statusLabelCenter` (ToolStripStatusLabel) - Alternating profile/station info
- `statusLabelRight` (ToolStripStatusLabel) - Event feed
**Buttons:**
- `connect_button` - "Connect" button (wire to `btnConnect_Click`)
- `disconnect_button` - "Disconnect" button (wire to `btnDisconnect_Click`)
- `stations_button` - "Stations" button (wire to `btnStations_Click`)
**GroupBox (groupBox1) - "Playback Controls":**
- `btnPlayPause` - "Play / Pause" button (wire to `btnPlayPause_Click`)
- `btnSkip` - "Skip" button (wire to `btnSkip_Click`)
**Additional Controls:**
- `album_art_picture_box` (PictureBox) - For album art display
- `info_table_Panel` (TableLayoutPanel) - For song metadata display
### SettingsForm Designer Controls Required
Open `Settings.cs` in the Form Designer and add these controls:
**GroupBox (profiles_group_box) - "Profile Management":**
- `selected_profile_label` - Label: "Selected profile:"
- `profiles_combo_box_selector` (ComboBox) - Profile selector (wire to `profiles_combo_box_selector_SelectedIndexChanged`)
- `new_profile_button` - "+ New" button (wire to `new_profile_button_Click`)
- `delete_profile_button` - "Delete" button (wire to `delete_profile_button_Click`)
**GroupBox (profile_configuration_group_box) - "Profile Configuration":**
**TabControl (profile_config_tab_control):**
**Tab 1 (outputs_tab_page) - "Outputs":**
- `obs_text_output_check_box` - "File output (For OBS / Streaming)"
- `output_label` - "Output Folder:"
- `output_folder_text_box` (TextBox)
- `output_folder_browse_button` - "Browse" (wire to `output_folder_browse_button_Click`)
- `local_web_server_check_box` - "Enable Local Web Server"
- `port_label` - "Port:"
- `textBox1` (TextBox) - Port number input
- `chk_toast_notifications` - "Show Windows Toast Notifications"
- `chkSMTC` - "Windows System Media Controls (SMTC)"
**Tab 2 (pianobar_override_tab_page) - "Pianobar Config & Auth":**
- `chkOverrideCredentials` - "Enable Profile Credentials..." (wire to `chkOverrideCredentials_CheckedChanged`)
- `txtConfigFilePathLabel` - "Config Save Path:"
- `txtConfigFilePath` (TextBox)
- `btnBrowseConfigFile` - "Browse" (wire to `btnBrowseConfigFile_Click`)
- `UsernameLabel` - "Username / Email:"
- `txtPianobarUser` (TextBox)
- `PasswordLabel` - "Password:"
- `txtPianobarPass` (TextBox) - Set UseSystemPasswordChar = true
**Dialog Buttons:**
- Add OK/Cancel buttons and wire appropriately to `btnSave_Click` and `btnCancel_Click`
### StationForm Designer Controls Required
Open `StationForm.cs` in the Form Designer and add these controls:
**GroupBox (station_list_group_box) - "Available Stations":**
- `search_label` - "Search:"
- `textBox1` (TextBox) - Search box (wire to `textBox1_TextChanged`)
- `lstStations` (ListBox) - Station list
**Buttons:**
- `refresh_button` - "Refresh stations" (optional)
- `btnSelectStation` - "Select Station" (wire to `btnSelectStation_Click`)
- `btnCancelStation` - "Cancel" (wire to `btnCancelStation_Click`)
## Quick Fix Steps
1. **Rebuild Designer Files:**
- Open Visual Studio
- Open each form (Main.cs, Settings.cs, StationForm.cs) in the Designer
- Right-click the form surface ? "View Code"
- Close and reopen the Designer view
- This should regenerate the Designer files properly
2. **Alternative - Manual Constructor Fix:**
If the Designer files are corrupted, you can temporarily comment out the constructors in the .cs files and let Visual Studio regenerate them.
3. **Verify Event Wiring:**
- In the Designer, select each control
- In Properties window, click the Events icon (lightning bolt)
- Double-click the appropriate event (e.g., Click, TextChanged)
- This will wire up the event handlers automatically
## Usage Instructions
### First Run
1. Launch the application
2. Go to File ? Settings
3. Create a new profile (click "+ New")
4. Configure output settings (text file, web server, etc.)
5. (Optional) Enable credential override and set Pianobar username/password
6. Click OK to save
### Connecting to Pianobar
1. Ensure `pianobar.exe` is in your PATH or specify full path
2. Click "Connect" button
3. Status indicator will show:
- ?? Red = Disconnected
- ?? Green = Connected (no outputs active)
- ?? Blue = Connected (outputs active)
### Playback Control
- Use "Play / Pause" button to toggle playback
- Use "Skip" button to skip current track
- Click "Stations" to open station picker
### Profile Switching
- Go to File ? Profiles
- Select a different profile (checkmark shows active)
- Reconnect to apply new profile settings
## Technical Details
### Status Bar Behavior
The center status zone alternates every 10 seconds between:
- Profile name display
- Current station display
### Output Mechanisms
**Text File Output:**
- Writes song metadata to specified text file
- Updates on each song change
- Format: Title, Artist, Album, Album Art URL, Timestamp
**Web Server:**
- Starts HTTP listener on specified port
- Serves JSON at `http://localhost:<port>/`
- JSON structure: `{ title, artist, album, albumArtUrl, timestamp }`
**Toast Notifications & SMTC:**
- Currently placeholder implementations
- Requires Windows SDK references to fully implement
- See TODO comments in OutputManager.cs
### Pianobar Communication Protocol
The app listens for these patterns in Pianobar output:
- Song metadata: Lines containing "Title:", "Artist:", "Album:", "coverArt:"
- Playing indicator: `|>` prefix
- Paused indicator: `||` prefix
- Station selection: `| Station "..."`
## Troubleshooting
### "InitializeComponent does not exist"
- The Designer files need to be regenerated
- Open each form in the Designer view
- Save the forms to regenerate Designer code
### "Pianobar is already running"
- Close existing Pianobar instances
- The app detects running processes to avoid conflicts
### Config file not generated
- Ensure "Override Credentials" is checked
- Verify config file path is valid
- Check write permissions to the target directory
### Web server won't start
- Check if port is already in use
- Try a different port number
- Run as administrator if using port < 1024
## Future Enhancements
1. **SMTC Integration:**
- Add reference to Windows.Media
- Implement SystemMediaTransportControls
- Handle media key events
2. **Toast Notifications:**
- Add reference to Windows.UI.Notifications
- Implement ToastNotificationManager
- Create custom toast templates with album art
3. **Album Art Display:**
- Download album art from URL
- Display in `album_art_picture_box`
- Cache images locally
4. **Song Info Display:**
- Populate `info_table_Panel` with metadata
- Show song progress
- Display station info
## File Structure
```
PainoBar Helper/
??? Main.cs # Main form implementation
??? Main.Designer.cs # Main form designer (regenerate)
??? Settings.cs # Settings form implementation
??? Settings.Designer.cs # Settings designer (regenerate)
??? StationForm.cs # Station picker implementation
??? StationForm.Designer.cs # Station designer (regenerate)
??? ProfileManager.cs # Profile management backend
??? PianobarManager.cs # Pianobar process manager
??? OutputManager.cs # Output mechanisms handler
??? Program.cs # Application entry point
??? IMPLEMENTATION_GUIDE.md # This file
```
## Contact & Support
For issues or questions about this implementation, refer to the inline code comments or the method documentation in each class.
All public methods include XML documentation comments explaining their purpose and parameters.
+36
View File
@@ -37,12 +37,16 @@
connect_button = new Button(); connect_button = new Button();
disconnect_button = new Button(); disconnect_button = new Button();
statusStrip1 = new StatusStrip(); statusStrip1 = new StatusStrip();
statusLabelLeft = new ToolStripStatusLabel();
statusLabelCenter = new ToolStripStatusLabel();
statusLabelRight = new ToolStripStatusLabel();
stations_button = new Button(); stations_button = new Button();
groupBox1 = new GroupBox(); groupBox1 = new GroupBox();
btnPlayPause = new Button(); btnPlayPause = new Button();
btnSkip = new Button(); btnSkip = new Button();
menuStrip1.SuspendLayout(); menuStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)album_art_picture_box).BeginInit(); ((System.ComponentModel.ISupportInitialize)album_art_picture_box).BeginInit();
statusStrip1.SuspendLayout();
groupBox1.SuspendLayout(); groupBox1.SuspendLayout();
SuspendLayout(); SuspendLayout();
// //
@@ -67,6 +71,7 @@
settingsToolStripMenuItem.Name = "settingsToolStripMenuItem"; settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
settingsToolStripMenuItem.Size = new Size(124, 22); settingsToolStripMenuItem.Size = new Size(124, 22);
settingsToolStripMenuItem.Text = "Settings"; settingsToolStripMenuItem.Text = "Settings";
settingsToolStripMenuItem.Click += menuSettings_Click;
// //
// profilesToolStripMenuItem // profilesToolStripMenuItem
// //
@@ -106,6 +111,7 @@
connect_button.TabIndex = 4; connect_button.TabIndex = 4;
connect_button.Text = "Connect"; connect_button.Text = "Connect";
connect_button.UseVisualStyleBackColor = true; connect_button.UseVisualStyleBackColor = true;
connect_button.Click += btnConnect_Click;
// //
// disconnect_button // disconnect_button
// //
@@ -115,15 +121,37 @@
disconnect_button.TabIndex = 5; disconnect_button.TabIndex = 5;
disconnect_button.Text = "Disconnect"; disconnect_button.Text = "Disconnect";
disconnect_button.UseVisualStyleBackColor = true; disconnect_button.UseVisualStyleBackColor = true;
disconnect_button.Click += btnDisconnect_Click;
// //
// statusStrip1 // statusStrip1
// //
statusStrip1.Items.AddRange(new ToolStripItem[] { statusLabelLeft, statusLabelCenter, statusLabelRight });
statusStrip1.Location = new Point(0, 377); statusStrip1.Location = new Point(0, 377);
statusStrip1.Name = "statusStrip1"; statusStrip1.Name = "statusStrip1";
statusStrip1.Size = new Size(666, 22); statusStrip1.Size = new Size(666, 22);
statusStrip1.TabIndex = 6; statusStrip1.TabIndex = 6;
statusStrip1.Text = "statusStrip1"; statusStrip1.Text = "statusStrip1";
// //
// statusLabelLeft
//
statusLabelLeft.Name = "statusLabelLeft";
statusLabelLeft.Size = new Size(100, 17);
statusLabelLeft.Text = "● Disconnected";
statusLabelLeft.ForeColor = Color.Red;
//
// statusLabelCenter
//
statusLabelCenter.Name = "statusLabelCenter";
statusLabelCenter.Size = new Size(451, 17);
statusLabelCenter.Spring = true;
statusLabelCenter.Text = "Profile: None";
//
// statusLabelRight
//
statusLabelRight.Name = "statusLabelRight";
statusLabelRight.Size = new Size(100, 17);
statusLabelRight.Text = "Ready";
//
// stations_button // stations_button
// //
stations_button.Location = new Point(385, 253); stations_button.Location = new Point(385, 253);
@@ -132,6 +160,7 @@
stations_button.TabIndex = 7; stations_button.TabIndex = 7;
stations_button.Text = "Stations"; stations_button.Text = "Stations";
stations_button.UseVisualStyleBackColor = true; stations_button.UseVisualStyleBackColor = true;
stations_button.Click += btnStations_Click;
// //
// groupBox1 // groupBox1
// //
@@ -152,6 +181,7 @@
btnPlayPause.TabIndex = 0; btnPlayPause.TabIndex = 0;
btnPlayPause.Text = "Play / Pause"; btnPlayPause.Text = "Play / Pause";
btnPlayPause.UseVisualStyleBackColor = true; btnPlayPause.UseVisualStyleBackColor = true;
btnPlayPause.Click += btnPlayPause_Click;
// //
// btnSkip // btnSkip
// //
@@ -161,6 +191,7 @@
btnSkip.TabIndex = 1; btnSkip.TabIndex = 1;
btnSkip.Text = "Skip"; btnSkip.Text = "Skip";
btnSkip.UseVisualStyleBackColor = true; btnSkip.UseVisualStyleBackColor = true;
btnSkip.Click += btnSkip_Click;
// //
// Main // Main
// //
@@ -181,6 +212,8 @@
menuStrip1.ResumeLayout(false); menuStrip1.ResumeLayout(false);
menuStrip1.PerformLayout(); menuStrip1.PerformLayout();
((System.ComponentModel.ISupportInitialize)album_art_picture_box).EndInit(); ((System.ComponentModel.ISupportInitialize)album_art_picture_box).EndInit();
statusStrip1.ResumeLayout(false);
statusStrip1.PerformLayout();
groupBox1.ResumeLayout(false); groupBox1.ResumeLayout(false);
ResumeLayout(false); ResumeLayout(false);
PerformLayout(); PerformLayout();
@@ -201,5 +234,8 @@
private GroupBox groupBox1; private GroupBox groupBox1;
private Button btnPlayPause; private Button btnPlayPause;
private Button btnSkip; private Button btnSkip;
private ToolStripStatusLabel statusLabelLeft;
private ToolStripStatusLabel statusLabelCenter;
private ToolStripStatusLabel statusLabelRight;
} }
} }
+395
View File
@@ -2,9 +2,404 @@ namespace PainoBar_Helper
{ {
public partial class Main : Form public partial class Main : Form
{ {
private readonly ProfileManager _profileManager;
private readonly PianobarManager _pianobarManager;
private OutputManager? _outputManager;
private System.Windows.Forms.Timer? _statusTimer;
private bool _statusToggle = false;
// Connection status enum
private enum ConnectionStatus
{
Disconnected, // Red
ConnectedIdle, // Green
ConnectedActive // Blue
}
private ConnectionStatus _currentStatus = ConnectionStatus.Disconnected;
public Main() public Main()
{ {
InitializeComponent(); InitializeComponent();
_profileManager = new ProfileManager();
_pianobarManager = new PianobarManager();
InitializeMenus();
InitializeStatusBar();
InitializeEvents();
UpdateProfilesMenu();
UpdateConnectionStatus(ConnectionStatus.Disconnected);
} }
#region Initialization
private void InitializeMenus()
{
// File menu is assumed to be created in the Designer
// This method sets up dynamic profile menu items
}
private void InitializeStatusBar()
{
// Status bar zones are assumed to be created in the Designer:
// - statusLabelLeft: Connection indicator
// - statusLabelCenter: Alternating status (Profile/Station)
// - statusLabelRight: Event feed
_statusTimer = new System.Windows.Forms.Timer();
_statusTimer.Interval = 10000; // 10 seconds
_statusTimer.Tick += StatusTimer_Tick;
_statusTimer.Start();
UpdateCenterStatus();
}
private void InitializeEvents()
{
// Pianobar events
_pianobarManager.Connected += PianobarManager_Connected;
_pianobarManager.Disconnected += PianobarManager_Disconnected;
_pianobarManager.SongChanged += PianobarManager_SongChanged;
_pianobarManager.StationChanged += PianobarManager_StationChanged;
_pianobarManager.EventOccurred += PianobarManager_EventOccurred;
// Profile events
_profileManager.ProfilesChanged += ProfileManager_ProfilesChanged;
_profileManager.ActiveProfileChanged += ProfileManager_ActiveProfileChanged;
}
#endregion
#region Button Event Handlers
// This method should be wired to btnConnect.Click in the Designer
public void btnConnect_Click(object? sender, EventArgs e)
{
if (_pianobarManager.IsConnected)
{
MessageBox.Show("Already connected to Pianobar.", "Already Connected",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
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();
_pianobarManager.Connect(pianobarPath, configPath);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to connect to Pianobar:\n{ex.Message}", "Connection Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// This method should be wired to btnDisconnect.Click in the Designer
public void btnDisconnect_Click(object? sender, EventArgs e)
{
if (!_pianobarManager.IsConnected)
{
MessageBox.Show("Not connected to Pianobar.", "Not Connected",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
_pianobarManager.Disconnect();
_outputManager?.Stop();
}
// This method should be wired to btnPlayPause.Click in the Designer
public void btnPlayPause_Click(object? sender, EventArgs e)
{
if (!_pianobarManager.IsConnected)
{
MessageBox.Show("Not connected to Pianobar.", "Not Connected",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
_pianobarManager.PlayPause();
UpdatePlayPauseButton();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to toggle play/pause:\n{ex.Message}", "Command Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// This method should be wired to btnSkip.Click in the Designer
public void btnSkip_Click(object? sender, EventArgs e)
{
if (!_pianobarManager.IsConnected)
{
MessageBox.Show("Not connected to Pianobar.", "Not Connected",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
_pianobarManager.Skip();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to skip track:\n{ex.Message}", "Command Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// This method should be wired to btnStations.Click in the Designer (or menu item)
public void btnStations_Click(object? sender, EventArgs e)
{
if (!_pianobarManager.IsConnected)
{
MessageBox.Show("Not connected to Pianobar.", "Not Connected",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var stationForm = new StationForm(_pianobarManager);
stationForm.ShowDialog(this);
}
#endregion
#region Menu Event Handlers
// This method should be wired to the Settings menu item click
public void menuSettings_Click(object? sender, EventArgs e)
{
var activeProfile = _profileManager.GetActiveProfile();
var settingsForm = new SettingsForm(_profileManager, activeProfile?.Settings ?? new ProfileSettings());
if (settingsForm.ShowDialog(this) == DialogResult.OK)
{
// Settings were saved, restart output manager if connected
if (_pianobarManager.IsConnected)
{
RestartOutputManager();
}
}
}
private void ProfileManager_ProfilesChanged(object? sender, EventArgs e)
{
UpdateProfilesMenu();
}
private void ProfileManager_ActiveProfileChanged(object? sender, EventArgs e)
{
UpdateProfilesMenu();
UpdateCenterStatus();
}
// Call this method to build the Profiles submenu dynamically
private void UpdateProfilesMenu()
{
// Assumes menuProfiles (profilesToolStripMenuItem) is a ToolStripMenuItem in your File menu
// Created in the Designer
if (profilesToolStripMenuItem == null)
return;
profilesToolStripMenuItem.DropDownItems.Clear();
foreach (var profile in _profileManager.Profiles)
{
var menuItem = new ToolStripMenuItem(profile.Name);
menuItem.Tag = profile.Name;
menuItem.Click += ProfileMenuItem_Click;
if (profile.Name == _profileManager.ActiveProfileName)
menuItem.Checked = true;
profilesToolStripMenuItem.DropDownItems.Add(menuItem);
}
}
private void ProfileMenuItem_Click(object? sender, EventArgs e)
{
if (sender is ToolStripMenuItem menuItem && menuItem.Tag is string profileName)
{
_profileManager.SetActiveProfile(profileName);
}
}
#endregion
#region Pianobar Event Handlers
private void PianobarManager_Connected(object? sender, EventArgs e)
{
Invoke(() =>
{
var activeProfile = _profileManager.GetActiveProfile();
if (activeProfile != null)
{
_outputManager = new OutputManager(activeProfile.Settings);
_outputManager.ActiveStateChanged += OutputManager_ActiveStateChanged;
_outputManager.Start();
}
UpdateConnectionStatus(
_outputManager?.IsActive == true ? ConnectionStatus.ConnectedActive : ConnectionStatus.ConnectedIdle
);
UpdatePlayPauseButton();
});
}
private void PianobarManager_Disconnected(object? sender, EventArgs e)
{
Invoke(() =>
{
_outputManager?.Stop();
_outputManager = null;
UpdateConnectionStatus(ConnectionStatus.Disconnected);
UpdatePlayPauseButton();
});
}
private void PianobarManager_SongChanged(object? sender, SongChangedEventArgs e)
{
Invoke(() =>
{
_outputManager?.UpdateSongMetadata(e);
});
}
private void PianobarManager_StationChanged(object? sender, string e)
{
// Station change is handled via event feed
UpdateCenterStatus();
}
private void PianobarManager_EventOccurred(object? sender, PianobarEventArgs e)
{
Invoke(() =>
{
// Update right status zone with event text
if (statusLabelRight != null)
{
statusLabelRight.Text = e.EventText;
}
});
}
private void OutputManager_ActiveStateChanged(object? sender, EventArgs e)
{
Invoke(() =>
{
if (_pianobarManager.IsConnected)
{
UpdateConnectionStatus(
_outputManager?.IsActive == true ? ConnectionStatus.ConnectedActive : ConnectionStatus.ConnectedIdle
);
}
});
}
#endregion
#region UI Update Methods
private void UpdateConnectionStatus(ConnectionStatus status)
{
_currentStatus = status;
if (statusLabelLeft == null)
return;
switch (status)
{
case ConnectionStatus.Disconnected:
statusLabelLeft.Text = "? Disconnected";
statusLabelLeft.ForeColor = Color.Red;
break;
case ConnectionStatus.ConnectedIdle:
statusLabelLeft.Text = "? Connected (Idle)";
statusLabelLeft.ForeColor = Color.Green;
break;
case ConnectionStatus.ConnectedActive:
statusLabelLeft.Text = "? Connected (Active)";
statusLabelLeft.ForeColor = Color.Blue;
break;
}
}
private void UpdatePlayPauseButton()
{
// Uses btnPlayPause from the Designer
if (btnPlayPause == null)
return;
if (!_pianobarManager.IsConnected)
{
btnPlayPause.Text = "Play";
return;
}
btnPlayPause.Text = _pianobarManager.IsPlaying ? "Pause" : "Play";
}
private void StatusTimer_Tick(object? sender, EventArgs e)
{
_statusToggle = !_statusToggle;
UpdateCenterStatus();
}
private void UpdateCenterStatus()
{
if (statusLabelCenter == null)
return;
if (_statusToggle)
{
// Show profile
string profileName = _profileManager.ActiveProfileName ?? "None";
statusLabelCenter.Text = $"Profile: {profileName}";
}
else
{
// Show station
string station = _pianobarManager.CurrentStation ?? "None";
statusLabelCenter.Text = $"Station: {station}";
}
}
private void RestartOutputManager()
{
_outputManager?.Stop();
var activeProfile = _profileManager.GetActiveProfile();
if (activeProfile != null && _pianobarManager.IsConnected)
{
_outputManager = new OutputManager(activeProfile.Settings);
_outputManager.ActiveStateChanged += OutputManager_ActiveStateChanged;
_outputManager.Start();
UpdateConnectionStatus(
_outputManager.IsActive ? ConnectionStatus.ConnectedActive : ConnectionStatus.ConnectedIdle
);
}
}
#endregion
#region Cleanup
protected override void OnFormClosing(FormClosingEventArgs e)
{
_statusTimer?.Stop();
_pianobarManager?.Disconnect();
_outputManager?.Dispose();
base.OnFormClosing(e);
}
#endregion
} }
} }
+216
View File
@@ -0,0 +1,216 @@
using System.Net;
using System.Text;
using System.Text.Json;
namespace PainoBar_Helper
{
/// <summary>
/// Manages various output mechanisms for song metadata
/// </summary>
public class OutputManager : IDisposable
{
private readonly ProfileSettings _settings;
private HttpListener? _webServer;
private string? _currentMetadataJson;
private bool _isActive;
public OutputManager(ProfileSettings settings)
{
_settings = settings;
}
public event EventHandler? ActiveStateChanged;
public bool IsActive
{
get => _isActive;
private set
{
if (_isActive != value)
{
_isActive = value;
ActiveStateChanged?.Invoke(this, EventArgs.Empty);
}
}
}
public void Start()
{
Stop();
if (_settings.EnableWebServer)
StartWebServer();
UpdateActiveState();
}
public void Stop()
{
StopWebServer();
UpdateActiveState();
}
public void UpdateSongMetadata(SongChangedEventArgs metadata)
{
var metadataObj = new
{
title = metadata.Title,
artist = metadata.Artist,
album = metadata.Album,
albumArtUrl = metadata.AlbumArtUrl,
timestamp = DateTime.Now
};
_currentMetadataJson = JsonSerializer.Serialize(metadataObj, new JsonSerializerOptions { WriteIndented = true });
if (_settings.EnableTextFile && !string.IsNullOrEmpty(_settings.TextFilePath))
WriteTextFile(metadata);
if (_settings.EnableToastNotifications)
ShowToastNotification(metadata);
if (_settings.EnableSMTC)
UpdateSMTC(metadata);
}
private void WriteTextFile(SongChangedEventArgs metadata)
{
try
{
var dir = Path.GetDirectoryName(_settings.TextFilePath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
var sb = new StringBuilder();
sb.AppendLine($"Title: {metadata.Title}");
sb.AppendLine($"Artist: {metadata.Artist}");
sb.AppendLine($"Album: {metadata.Album}");
sb.AppendLine($"Album Art URL: {metadata.AlbumArtUrl}");
sb.AppendLine($"Updated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
File.WriteAllText(_settings.TextFilePath, sb.ToString());
}
catch
{
// Silently fail
}
}
private void StartWebServer()
{
try
{
_webServer = new HttpListener();
_webServer.Prefixes.Add($"http://localhost:{_settings.WebServerPort}/");
_webServer.Start();
Task.Run(() => WebServerLoop());
}
catch
{
_webServer = null;
}
}
private void StopWebServer()
{
if (_webServer != null)
{
try
{
_webServer.Stop();
_webServer.Close();
}
catch { }
finally
{
_webServer = null;
}
}
}
private async void WebServerLoop()
{
while (_webServer != null && _webServer.IsListening)
{
try
{
var context = await _webServer.GetContextAsync();
HandleWebRequest(context);
}
catch
{
break;
}
}
}
private void HandleWebRequest(HttpListenerContext context)
{
try
{
var response = context.Response;
response.ContentType = "application/json";
response.ContentEncoding = Encoding.UTF8;
string responseData = _currentMetadataJson ?? "{}";
byte[] buffer = Encoding.UTF8.GetBytes(responseData);
response.ContentLength64 = buffer.Length;
response.OutputStream.Write(buffer, 0, buffer.Length);
response.OutputStream.Close();
}
catch { }
}
private void ShowToastNotification(SongChangedEventArgs metadata)
{
// Windows Toast Notification implementation placeholder
// In a full implementation, you would use Windows.UI.Notifications
// For now, this is a placeholder that can be extended
try
{
// TODO: Implement using Windows.UI.Notifications ToastNotificationManager
// This requires adding Windows SDK references
}
catch { }
}
private void UpdateSMTC(SongChangedEventArgs metadata)
{
// Windows System Media Transport Controls implementation placeholder
// In a full implementation, you would use Windows.Media.SystemMediaTransportControls
// For now, this is a placeholder that can be extended
try
{
// TODO: Implement using Windows.Media.SystemMediaTransportControls
// This requires adding Windows SDK references
}
catch { }
}
private void UpdateActiveState()
{
bool anyActive = false;
if (_settings.EnableTextFile && !string.IsNullOrEmpty(_settings.TextFilePath))
anyActive = true;
if (_settings.EnableWebServer && _webServer != null && _webServer.IsListening)
anyActive = true;
if (_settings.EnableToastNotifications)
anyActive = true;
if (_settings.EnableSMTC)
anyActive = true;
IsActive = anyActive;
}
public void Dispose()
{
Stop();
}
}
}
+328
View File
@@ -0,0 +1,328 @@
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
namespace PainoBar_Helper
{
/// <summary>
/// Manages the Pianobar process lifecycle and communication
/// </summary>
public class PianobarManager : IDisposable
{
private Process? _process;
private StreamWriter? _inputWriter;
private bool _isConnected;
private readonly object _lockObject = new object();
public event EventHandler<SongChangedEventArgs>? SongChanged;
public event EventHandler<string>? StationChanged;
public event EventHandler<PianobarEventArgs>? EventOccurred;
public event EventHandler? Connected;
public event EventHandler? Disconnected;
public event EventHandler<string>? OutputReceived;
public bool IsConnected
{
get { lock (_lockObject) return _isConnected; }
private set
{
bool changed = false;
lock (_lockObject)
{
if (_isConnected != value)
{
_isConnected = value;
changed = true;
}
}
if (changed)
{
if (value)
Connected?.Invoke(this, EventArgs.Empty);
else
Disconnected?.Invoke(this, EventArgs.Empty);
}
}
}
public string? CurrentSong { get; private set; }
public string? CurrentArtist { get; private set; }
public string? CurrentAlbum { get; private set; }
public string? CurrentAlbumArtUrl { get; private set; }
public string? CurrentStation { get; private set; }
public bool IsPlaying { get; private set; } = true;
public void Connect(string pianobarPath, string? configPath = null)
{
if (IsConnected)
throw new InvalidOperationException("Already connected to Pianobar.");
// Check if pianobar is already running
var existingProcesses = Process.GetProcessesByName("pianobar");
if (existingProcesses.Length > 0)
{
foreach (var proc in existingProcesses)
proc.Dispose();
throw new InvalidOperationException("Pianobar is already running. Please close existing instances.");
}
try
{
var startInfo = new ProcessStartInfo
{
FileName = pianobarPath,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
if (!string.IsNullOrEmpty(configPath))
{
startInfo.Arguments = $"-c \"{configPath}\"";
}
_process = new Process { StartInfo = startInfo };
_process.OutputDataReceived += Process_OutputDataReceived;
_process.ErrorDataReceived += Process_ErrorDataReceived;
_process.Exited += Process_Exited;
_process.EnableRaisingEvents = true;
_process.Start();
_inputWriter = _process.StandardInput;
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
IsConnected = true;
RaiseEvent("Connected to Pianobar");
}
catch (Exception ex)
{
Cleanup();
throw new InvalidOperationException($"Failed to start Pianobar: {ex.Message}", ex);
}
}
public void Disconnect()
{
if (!IsConnected)
return;
Cleanup();
IsConnected = false;
RaiseEvent("Disconnected from Pianobar");
}
public void SendCommand(char command)
{
if (!IsConnected || _inputWriter == null)
throw new InvalidOperationException("Not connected to Pianobar.");
try
{
_inputWriter.WriteLine(command);
_inputWriter.Flush();
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to send command: {ex.Message}", ex);
}
}
public void SendCommand(string command)
{
if (!IsConnected || _inputWriter == null)
throw new InvalidOperationException("Not connected to Pianobar.");
try
{
_inputWriter.WriteLine(command);
_inputWriter.Flush();
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to send command: {ex.Message}", ex);
}
}
public void PlayPause()
{
SendCommand('p');
IsPlaying = !IsPlaying;
RaiseEvent(IsPlaying ? "Resumed" : "Paused");
}
public void Skip()
{
SendCommand('n');
RaiseEvent("Skipped Track");
}
public void ShowStations()
{
SendCommand('s');
}
private void Process_OutputDataReceived(object? sender, DataReceivedEventArgs e)
{
if (string.IsNullOrEmpty(e.Data))
return;
string line = e.Data;
OutputReceived?.Invoke(this, line);
ParseOutput(line);
}
private void Process_ErrorDataReceived(object? sender, DataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data))
OutputReceived?.Invoke(this, e.Data);
}
private void Process_Exited(object? sender, EventArgs e)
{
IsConnected = false;
RaiseEvent("Pianobar process exited");
}
private void ParseOutput(string line)
{
try
{
// Parse song information
if (line.Contains("|>"))
{
// Song is playing
IsPlaying = true;
ParseSongLine(line);
}
else if (line.Contains("||"))
{
// Song is paused
IsPlaying = false;
}
// Parse station selection
var stationMatch = Regex.Match(line, @"\|\s*Station\s+""(.+?)""");
if (stationMatch.Success)
{
CurrentStation = stationMatch.Groups[1].Value;
StationChanged?.Invoke(this, CurrentStation);
RaiseEvent("Station Selected");
}
// Parse song metadata
if (line.Contains("Title:"))
{
var match = Regex.Match(line, @"Title:\s*(.+)");
if (match.Success)
CurrentSong = match.Groups[1].Value.Trim();
}
else if (line.Contains("Artist:"))
{
var match = Regex.Match(line, @"Artist:\s*(.+)");
if (match.Success)
CurrentArtist = match.Groups[1].Value.Trim();
}
else if (line.Contains("Album:"))
{
var match = Regex.Match(line, @"Album:\s*(.+)");
if (match.Success)
CurrentAlbum = match.Groups[1].Value.Trim();
}
else if (line.Contains("coverArt"))
{
var match = Regex.Match(line, @"coverArt:\s*(.+)");
if (match.Success)
{
CurrentAlbumArtUrl = match.Groups[1].Value.Trim();
TriggerSongChanged();
}
}
}
catch
{
// Ignore parsing errors
}
}
private void ParseSongLine(string line)
{
// Parse format: |> "Song Title" by "Artist" on "Album"
var match = Regex.Match(line, @"\|>\s+""(.+?)""\s+by\s+""(.+?)""\s+on\s+""(.+?)""");
if (match.Success)
{
CurrentSong = match.Groups[1].Value;
CurrentArtist = match.Groups[2].Value;
CurrentAlbum = match.Groups[3].Value;
TriggerSongChanged();
}
}
private void TriggerSongChanged()
{
if (!string.IsNullOrEmpty(CurrentSong) && !string.IsNullOrEmpty(CurrentArtist))
{
SongChanged?.Invoke(this, new SongChangedEventArgs
{
Title = CurrentSong,
Artist = CurrentArtist,
Album = CurrentAlbum,
AlbumArtUrl = CurrentAlbumArtUrl
});
RaiseEvent("Song Changed");
}
}
private void RaiseEvent(string eventText)
{
EventOccurred?.Invoke(this, new PianobarEventArgs { EventText = eventText });
}
private void Cleanup()
{
try
{
if (_process != null)
{
_process.OutputDataReceived -= Process_OutputDataReceived;
_process.ErrorDataReceived -= Process_ErrorDataReceived;
_process.Exited -= Process_Exited;
_inputWriter?.Close();
_inputWriter = null;
// Don't kill the process, just detach
_process.Close();
_process = null;
}
}
catch
{
// Ignore cleanup errors
}
}
public void Dispose()
{
Disconnect();
}
}
public class SongChangedEventArgs : EventArgs
{
public string? Title { get; set; }
public string? Artist { get; set; }
public string? Album { get; set; }
public string? AlbumArtUrl { get; set; }
}
public class PianobarEventArgs : EventArgs
{
public string EventText { get; set; } = string.Empty;
}
}
+219
View File
@@ -0,0 +1,219 @@
using System.Text;
using System.Text.Json;
namespace PainoBar_Helper
{
/// <summary>
/// Manages Pianobar profiles including creation, deletion, and configuration file generation
/// </summary>
public class ProfileManager
{
private const string ProfilesFileName = "profiles.json";
private readonly string _profilesFilePath;
private List<Profile> _profiles;
private string? _activeProfileName;
public ProfileManager()
{
string appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"PianobarHelper"
);
Directory.CreateDirectory(appDataPath);
_profilesFilePath = Path.Combine(appDataPath, ProfilesFileName);
_profiles = new List<Profile>();
LoadProfiles();
}
public event EventHandler? ProfilesChanged;
public event EventHandler? ActiveProfileChanged;
public IReadOnlyList<Profile> Profiles => _profiles.AsReadOnly();
public string? ActiveProfileName => _activeProfileName;
public Profile? GetActiveProfile()
{
if (string.IsNullOrEmpty(_activeProfileName))
return null;
return _profiles.FirstOrDefault(p => p.Name == _activeProfileName);
}
public void SetActiveProfile(string profileName)
{
var profile = _profiles.FirstOrDefault(p => p.Name == profileName);
if (profile == null)
throw new ArgumentException($"Profile '{profileName}' does not exist.");
_activeProfileName = profileName;
SaveProfiles();
ActiveProfileChanged?.Invoke(this, EventArgs.Empty);
}
public void CreateProfile(string name, ProfileSettings settings)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Profile name cannot be empty.");
if (_profiles.Any(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException($"Profile '{name}' already exists.");
var profile = new Profile
{
Name = name,
Settings = settings
};
_profiles.Add(profile);
if (_profiles.Count == 1)
_activeProfileName = name;
SaveProfiles();
ProfilesChanged?.Invoke(this, EventArgs.Empty);
if (settings.OverrideCredentials && !string.IsNullOrEmpty(settings.ConfigFilePath))
GenerateConfigFile(profile);
}
public void UpdateProfile(string name, ProfileSettings settings)
{
var profile = _profiles.FirstOrDefault(p => p.Name == name);
if (profile == null)
throw new ArgumentException($"Profile '{name}' does not exist.");
profile.Settings = settings;
SaveProfiles();
ProfilesChanged?.Invoke(this, EventArgs.Empty);
if (settings.OverrideCredentials && !string.IsNullOrEmpty(settings.ConfigFilePath))
GenerateConfigFile(profile);
}
public void DeleteProfile(string name)
{
var profile = _profiles.FirstOrDefault(p => p.Name == name);
if (profile == null)
return;
_profiles.Remove(profile);
if (_activeProfileName == name)
{
_activeProfileName = _profiles.FirstOrDefault()?.Name;
ActiveProfileChanged?.Invoke(this, EventArgs.Empty);
}
SaveProfiles();
ProfilesChanged?.Invoke(this, EventArgs.Empty);
}
private void LoadProfiles()
{
if (!File.Exists(_profilesFilePath))
{
_profiles = new List<Profile>();
return;
}
try
{
string json = File.ReadAllText(_profilesFilePath);
var data = JsonSerializer.Deserialize<ProfileData>(json);
if (data != null)
{
_profiles = data.Profiles ?? new List<Profile>();
_activeProfileName = data.ActiveProfile;
}
}
catch
{
_profiles = new List<Profile>();
}
}
private void SaveProfiles()
{
var data = new ProfileData
{
Profiles = _profiles,
ActiveProfile = _activeProfileName
};
string json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_profilesFilePath, json);
}
private void GenerateConfigFile(Profile profile)
{
var settings = profile.Settings;
if (string.IsNullOrEmpty(settings.ConfigFilePath))
return;
try
{
var configDir = Path.GetDirectoryName(settings.ConfigFilePath);
if (!string.IsNullOrEmpty(configDir))
Directory.CreateDirectory(configDir);
var sb = new StringBuilder();
sb.AppendLine("# Pianobar configuration file");
sb.AppendLine($"# Generated by Pianobar Helper for profile: {profile.Name}");
sb.AppendLine();
if (settings.OverrideCredentials)
{
if (!string.IsNullOrEmpty(settings.PianobarUser))
sb.AppendLine($"user = {settings.PianobarUser}");
if (!string.IsNullOrEmpty(settings.PianobarPassword))
sb.AppendLine($"password = {settings.PianobarPassword}");
sb.AppendLine();
}
File.WriteAllText(settings.ConfigFilePath, sb.ToString());
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to generate config file: {ex.Message}", ex);
}
}
public string? GetActiveConfigPath()
{
var activeProfile = GetActiveProfile();
if (activeProfile?.Settings.OverrideCredentials == true)
return activeProfile.Settings.ConfigFilePath;
return null;
}
}
public class Profile
{
public string Name { get; set; } = string.Empty;
public ProfileSettings Settings { get; set; } = new ProfileSettings();
}
public class ProfileSettings
{
// Output Settings
public bool EnableTextFile { get; set; }
public string TextFilePath { get; set; } = string.Empty;
public bool EnableWebServer { get; set; }
public int WebServerPort { get; set; } = 8080;
public bool EnableToastNotifications { get; set; }
public bool EnableSMTC { get; set; }
// Pianobar Config & Auth
public bool OverrideCredentials { get; set; }
public string ConfigFilePath { get; set; } = string.Empty;
public string PianobarUser { get; set; } = string.Empty;
public string PianobarPassword { get; set; } = string.Empty;
}
internal class ProfileData
{
public List<Profile>? Profiles { get; set; }
public string? ActiveProfile { get; set; }
}
}
+9 -3
View File
@@ -1,6 +1,6 @@
namespace PainoBar_Helper namespace PainoBar_Helper
{ {
partial class settings_form partial class SettingsForm
{ {
/// <summary> /// <summary>
/// Required designer variable. /// Required designer variable.
@@ -95,6 +95,7 @@
profiles_combo_box_selector.Name = "profiles_combo_box_selector"; profiles_combo_box_selector.Name = "profiles_combo_box_selector";
profiles_combo_box_selector.Size = new Size(199, 25); profiles_combo_box_selector.Size = new Size(199, 25);
profiles_combo_box_selector.TabIndex = 1; profiles_combo_box_selector.TabIndex = 1;
profiles_combo_box_selector.SelectedIndexChanged += profiles_combo_box_selector_SelectedIndexChanged;
// //
// new_profile_button // new_profile_button
// //
@@ -104,6 +105,7 @@
new_profile_button.TabIndex = 2; new_profile_button.TabIndex = 2;
new_profile_button.Text = "+ New"; new_profile_button.Text = "+ New";
new_profile_button.UseVisualStyleBackColor = true; new_profile_button.UseVisualStyleBackColor = true;
new_profile_button.Click += new_profile_button_Click;
// //
// delete_profile_button // delete_profile_button
// //
@@ -113,6 +115,7 @@
delete_profile_button.TabIndex = 3; delete_profile_button.TabIndex = 3;
delete_profile_button.Text = "Delete"; delete_profile_button.Text = "Delete";
delete_profile_button.UseVisualStyleBackColor = true; delete_profile_button.UseVisualStyleBackColor = true;
delete_profile_button.Click += delete_profile_button_Click;
// //
// profile_name_label // profile_name_label
// //
@@ -251,6 +254,7 @@
output_folder_browse_button.TabIndex = 3; output_folder_browse_button.TabIndex = 3;
output_folder_browse_button.Text = "Browse"; output_folder_browse_button.Text = "Browse";
output_folder_browse_button.UseVisualStyleBackColor = true; output_folder_browse_button.UseVisualStyleBackColor = true;
output_folder_browse_button.Click += output_folder_browse_button_Click;
// //
// local_web_server_check_box // local_web_server_check_box
// //
@@ -307,6 +311,7 @@
chkOverrideCredentials.TabIndex = 0; chkOverrideCredentials.TabIndex = 0;
chkOverrideCredentials.Text = "Enable Profile Credentials and Custom Config Generation"; chkOverrideCredentials.Text = "Enable Profile Credentials and Custom Config Generation";
chkOverrideCredentials.UseVisualStyleBackColor = true; chkOverrideCredentials.UseVisualStyleBackColor = true;
chkOverrideCredentials.CheckedChanged += chkOverrideCredentials_CheckedChanged;
// //
// txtConfigFilePathLabel // txtConfigFilePathLabel
// //
@@ -332,6 +337,7 @@
btnBrowseConfigFile.TabIndex = 3; btnBrowseConfigFile.TabIndex = 3;
btnBrowseConfigFile.Text = "Browse"; btnBrowseConfigFile.Text = "Browse";
btnBrowseConfigFile.UseVisualStyleBackColor = true; btnBrowseConfigFile.UseVisualStyleBackColor = true;
btnBrowseConfigFile.Click += btnBrowseConfigFile_Click;
// //
// UsernameLabel // UsernameLabel
// //
@@ -366,14 +372,14 @@
txtPianobarPass.TabIndex = 7; txtPianobarPass.TabIndex = 7;
txtPianobarPass.UseSystemPasswordChar = true; txtPianobarPass.UseSystemPasswordChar = true;
// //
// settings_form // SettingsForm
// //
AutoScaleDimensions = new SizeF(7F, 17F); AutoScaleDimensions = new SizeF(7F, 17F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(454, 586); ClientSize = new Size(454, 586);
Controls.Add(profile_configuration_group_box); Controls.Add(profile_configuration_group_box);
Controls.Add(profiles_group_box); Controls.Add(profiles_group_box);
Name = "settings_form"; Name = "SettingsForm";
Text = "Settings"; Text = "Settings";
profiles_group_box.ResumeLayout(false); profiles_group_box.ResumeLayout(false);
profiles_group_box.PerformLayout(); profiles_group_box.PerformLayout();
+344 -12
View File
@@ -1,20 +1,352 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace PainoBar_Helper namespace PainoBar_Helper
{ {
public partial class settings_form : Form public partial class SettingsForm : Form
{ {
public settings_form() private readonly ProfileManager _profileManager;
private ProfileSettings _currentSettings;
private string? _currentProfileName;
public SettingsForm(ProfileManager profileManager, ProfileSettings currentSettings)
{ {
InitializeComponent(); InitializeComponent();
_profileManager = profileManager;
_currentSettings = new ProfileSettings
{
EnableTextFile = currentSettings.EnableTextFile,
TextFilePath = currentSettings.TextFilePath,
EnableWebServer = currentSettings.EnableWebServer,
WebServerPort = currentSettings.WebServerPort,
EnableToastNotifications = currentSettings.EnableToastNotifications,
EnableSMTC = currentSettings.EnableSMTC,
OverrideCredentials = currentSettings.OverrideCredentials,
ConfigFilePath = currentSettings.ConfigFilePath,
PianobarUser = currentSettings.PianobarUser,
PianobarPassword = currentSettings.PianobarPassword
};
_currentProfileName = profileManager.ActiveProfileName;
InitializeControls();
} }
#region Initialization
private void InitializeControls()
{
LoadProfileList();
LoadSettingsToControls();
}
private void LoadProfileList()
{
// Uses profiles_combo_box_selector from the Designer
if (profiles_combo_box_selector == null)
return;
profiles_combo_box_selector.Items.Clear();
foreach (var profile in _profileManager.Profiles)
{
profiles_combo_box_selector.Items.Add(profile.Name);
}
if (!string.IsNullOrEmpty(_currentProfileName))
profiles_combo_box_selector.SelectedItem = _currentProfileName;
}
private void LoadSettingsToControls()
{
// Output Settings (Tab 1)
if (obs_text_output_check_box != null)
obs_text_output_check_box.Checked = _currentSettings.EnableTextFile;
if (output_folder_text_box != null)
output_folder_text_box.Text = _currentSettings.TextFilePath;
if (local_web_server_check_box != null)
local_web_server_check_box.Checked = _currentSettings.EnableWebServer;
if (textBox1 != null)
textBox1.Text = _currentSettings.WebServerPort.ToString();
if (chk_toast_notifications != null)
chk_toast_notifications.Checked = _currentSettings.EnableToastNotifications;
if (chkSMTC != null)
chkSMTC.Checked = _currentSettings.EnableSMTC;
// Pianobar Config (Tab 2)
if (chkOverrideCredentials != null)
{
chkOverrideCredentials.Checked = _currentSettings.OverrideCredentials;
ToggleCredentialFields(chkOverrideCredentials.Checked);
}
if (txtConfigFilePath != null)
txtConfigFilePath.Text = _currentSettings.ConfigFilePath;
if (txtPianobarUser != null)
txtPianobarUser.Text = _currentSettings.PianobarUser;
if (txtPianobarPass != null)
txtPianobarPass.Text = _currentSettings.PianobarPassword;
}
private void SaveSettingsFromControls()
{
// Output Settings
if (obs_text_output_check_box != null)
_currentSettings.EnableTextFile = obs_text_output_check_box.Checked;
if (output_folder_text_box != null)
_currentSettings.TextFilePath = output_folder_text_box.Text;
if (local_web_server_check_box != null)
_currentSettings.EnableWebServer = local_web_server_check_box.Checked;
if (textBox1 != null && int.TryParse(textBox1.Text, out int port))
_currentSettings.WebServerPort = port;
if (chk_toast_notifications != null)
_currentSettings.EnableToastNotifications = chk_toast_notifications.Checked;
if (chkSMTC != null)
_currentSettings.EnableSMTC = chkSMTC.Checked;
// Pianobar Config
if (chkOverrideCredentials != null)
_currentSettings.OverrideCredentials = chkOverrideCredentials.Checked;
if (txtConfigFilePath != null)
_currentSettings.ConfigFilePath = txtConfigFilePath.Text;
if (txtPianobarUser != null)
_currentSettings.PianobarUser = txtPianobarUser.Text;
if (txtPianobarPass != null)
_currentSettings.PianobarPassword = txtPianobarPass.Text;
}
#endregion
#region Profile Management
// 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)
return;
var profile = _profileManager.Profiles.FirstOrDefault(p => p.Name == profileName);
if (profile != null)
{
_currentProfileName = profileName;
_currentSettings = new ProfileSettings
{
EnableTextFile = profile.Settings.EnableTextFile,
TextFilePath = profile.Settings.TextFilePath,
EnableWebServer = profile.Settings.EnableWebServer,
WebServerPort = profile.Settings.WebServerPort,
EnableToastNotifications = profile.Settings.EnableToastNotifications,
EnableSMTC = profile.Settings.EnableSMTC,
OverrideCredentials = profile.Settings.OverrideCredentials,
ConfigFilePath = profile.Settings.ConfigFilePath,
PianobarUser = profile.Settings.PianobarUser,
PianobarPassword = profile.Settings.PianobarPassword
};
LoadSettingsToControls();
}
}
// 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:");
if (string.IsNullOrWhiteSpace(profileName))
return;
try
{
var newSettings = new ProfileSettings();
_profileManager.CreateProfile(profileName, newSettings);
LoadProfileList();
if (profiles_combo_box_selector != null)
profiles_combo_box_selector.SelectedItem = profileName;
MessageBox.Show($"Profile '{profileName}' created successfully.", "Profile Created",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to create profile:\n{ex.Message}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// Wire this to delete_profile_button.Click in the Designer
public void delete_profile_button_Click(object? sender, EventArgs e)
{
if (string.IsNullOrEmpty(_currentProfileName))
{
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}'?",
"Confirm Deletion", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
try
{
_profileManager.DeleteProfile(_currentProfileName);
_currentProfileName = _profileManager.ActiveProfileName;
LoadProfileList();
var activeProfile = _profileManager.GetActiveProfile();
if (activeProfile != null)
{
_currentSettings = activeProfile.Settings;
LoadSettingsToControls();
}
MessageBox.Show("Profile deleted successfully.", "Profile Deleted",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to delete profile:\n{ex.Message}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
#endregion
#region File Browsing
// Wire this to output_folder_browse_button.Click in the Designer
public void output_folder_browse_button_Click(object? sender, EventArgs e)
{
using var dialog = new SaveFileDialog
{
Filter = "Text Files (*.txt)|*.txt|All Files (*.*)|*.*",
Title = "Select Text File Output Path",
FileName = "now_playing.txt"
};
if (dialog.ShowDialog() == DialogResult.OK)
{
if (output_folder_text_box != null)
output_folder_text_box.Text = dialog.FileName;
}
}
// Wire this to btnBrowseConfigFile.Click in the Designer
public void btnBrowseConfigFile_Click(object? sender, EventArgs e)
{
using var dialog = new SaveFileDialog
{
Filter = "Config Files (*.config)|*.config|All Files (*.*)|*.*",
Title = "Select Pianobar Config File Path",
FileName = "pianobar.config"
};
if (dialog.ShowDialog() == DialogResult.OK)
{
if (txtConfigFilePath != null)
txtConfigFilePath.Text = dialog.FileName;
}
}
#endregion
#region Credential Override
// Wire this to chkOverrideCredentials.CheckedChanged in the Designer
public void chkOverrideCredentials_CheckedChanged(object? sender, EventArgs e)
{
if (sender is CheckBox chk)
ToggleCredentialFields(chk.Checked);
}
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;
}
#endregion
#region Save/Cancel
// Wire this to btnSave.Click in the Designer (or OK button)
public void btnSave_Click(object? sender, EventArgs e)
{
try
{
SaveSettingsFromControls();
if (!string.IsNullOrEmpty(_currentProfileName))
{
_profileManager.UpdateProfile(_currentProfileName, _currentSettings);
}
DialogResult = DialogResult.OK;
Close();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save settings:\n{ex.Message}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// Wire this to btnCancel.Click in the Designer (or Cancel button)
public void btnCancel_Click(object? sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
#endregion
#region Helper Methods
private string? PromptForProfileName(string title, string prompt)
{
using var form = new Form
{
Text = title,
Width = 400,
Height = 150,
StartPosition = FormStartPosition.CenterParent,
FormBorderStyle = FormBorderStyle.FixedDialog,
MaximizeBox = false,
MinimizeBox = false
};
var label = new Label { Text = prompt, Left = 10, Top = 20, Width = 360 };
var textBox = new TextBox { Left = 10, Top = 50, Width = 360 };
var btnOk = new Button { Text = "OK", Left = 210, Top = 80, DialogResult = DialogResult.OK };
var btnCancel = new Button { Text = "Cancel", Left = 290, Top = 80, DialogResult = DialogResult.Cancel };
form.Controls.AddRange(new Control[] { label, textBox, btnOk, btnCancel });
form.AcceptButton = btnOk;
form.CancelButton = btnCancel;
return form.ShowDialog() == DialogResult.OK ? textBox.Text : null;
}
#endregion
} }
} }
+3
View File
@@ -65,6 +65,7 @@
textBox1.Name = "textBox1"; textBox1.Name = "textBox1";
textBox1.Size = new Size(273, 24); textBox1.Size = new Size(273, 24);
textBox1.TabIndex = 1; textBox1.TabIndex = 1;
textBox1.TextChanged += textBox1_TextChanged;
// //
// lstStations // lstStations
// //
@@ -91,6 +92,7 @@
btnSelectStation.TabIndex = 2; btnSelectStation.TabIndex = 2;
btnSelectStation.Text = "Refresh stations"; btnSelectStation.Text = "Refresh stations";
btnSelectStation.UseVisualStyleBackColor = true; btnSelectStation.UseVisualStyleBackColor = true;
btnSelectStation.Click += btnSelectStation_Click;
// //
// btnCancelStation // btnCancelStation
// //
@@ -100,6 +102,7 @@
btnCancelStation.TabIndex = 3; btnCancelStation.TabIndex = 3;
btnCancelStation.Text = "Cancel"; btnCancelStation.Text = "Cancel";
btnCancelStation.UseVisualStyleBackColor = true; btnCancelStation.UseVisualStyleBackColor = true;
btnCancelStation.Click += btnCancelStation_Click;
// //
// StationForm // StationForm
// //
+211 -11
View File
@@ -1,20 +1,220 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace PainoBar_Helper namespace PainoBar_Helper
{ {
public partial class StationForm : Form public partial class StationForm : Form
{ {
public StationForm() private readonly PianobarManager _pianobarManager;
private List<string> _stations;
private List<string> _filteredStations;
private bool _stationsLoaded = false;
public StationForm(PianobarManager pianobarManager)
{ {
InitializeComponent(); InitializeComponent();
_pianobarManager = pianobarManager;
_stations = new List<string>();
_filteredStations = new List<string>();
InitializeControls();
LoadStations();
} }
#region Initialization
private void InitializeControls()
{
// Uses textBox1 (search box) and lstStations from the Designer
// Uses btnSelectStation from the Designer
if (lstStations != null)
{
lstStations.DoubleClick += LstStations_DoubleClick;
}
}
private void LoadStations()
{
// Subscribe to output to capture station list
_pianobarManager.OutputReceived += PianobarManager_OutputReceived;
// Send command to show stations
try
{
_pianobarManager.ShowStations();
// Show loading message
if (lstStations != null)
{
lstStations.Items.Clear();
lstStations.Items.Add("Loading stations...");
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load stations:\n{ex.Message}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
#endregion
#region Station Loading
private void PianobarManager_OutputReceived(object? sender, string e)
{
// Parse station list from Pianobar output
// Format is typically: "0) Station Name"
var match = System.Text.RegularExpressions.Regex.Match(e, @"^\s*(\d+)\)\s*(.+)$");
if (match.Success)
{
string stationName = match.Groups[2].Value.Trim();
if (!_stations.Contains(stationName))
{
_stations.Add(stationName);
Invoke(() => UpdateStationList());
}
}
// Detect end of station list
if (e.Contains("Select station:") || e.Contains("[?]"))
{
_stationsLoaded = true;
_pianobarManager.OutputReceived -= PianobarManager_OutputReceived;
Invoke(() => FinalizeStationList());
}
}
private void UpdateStationList()
{
if (lstStations == null)
return;
lstStations.Items.Clear();
foreach (var station in _filteredStations.Any() ? _filteredStations : _stations)
{
lstStations.Items.Add(station);
}
}
private void FinalizeStationList()
{
if (lstStations != null)
{
if (_stations.Count == 0)
{
lstStations.Items.Clear();
lstStations.Items.Add("No stations available");
}
}
}
#endregion
#region Search
// Wire this to textBox1.TextChanged in the Designer
public void textBox1_TextChanged(object? sender, EventArgs e)
{
if (sender is not TextBox txtSearch)
return;
string searchText = txtSearch.Text.Trim();
if (string.IsNullOrWhiteSpace(searchText))
{
_filteredStations.Clear();
}
else
{
_filteredStations = _stations
.Where(s => s.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0)
.ToList();
}
UpdateStationList();
}
#endregion
#region Station Selection
// Wire this to btnSelectStation.Click in the Designer
public void btnSelectStation_Click(object? sender, EventArgs e)
{
SelectCurrentStation();
}
private void LstStations_DoubleClick(object? sender, EventArgs e)
{
SelectCurrentStation();
}
private void SelectCurrentStation()
{
if (lstStations == null)
return;
if (lstStations.SelectedIndex < 0)
{
MessageBox.Show("Please select a station.", "No Selection",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
string selectedStation = lstStations.SelectedItem?.ToString() ?? string.Empty;
if (string.IsNullOrEmpty(selectedStation))
return;
// Find the index in the original station list
int stationIndex = _stations.IndexOf(selectedStation);
if (stationIndex < 0)
return;
try
{
// Send the station index to Pianobar
// Pianobar expects the station number (0-based index)
_pianobarManager.SendCommand(stationIndex.ToString());
DialogResult = DialogResult.OK;
Close();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to select station:\n{ex.Message}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
#endregion
#region Cancel
// Wire this to btnCancelStation.Click in the Designer (or Cancel button)
public void btnCancelStation_Click(object? sender, EventArgs e)
{
// Send 'q' to cancel station selection in Pianobar
try
{
_pianobarManager.SendCommand('q');
}
catch { }
DialogResult = DialogResult.Cancel;
Close();
}
#endregion
#region Cleanup
protected override void OnFormClosing(FormClosingEventArgs e)
{
_pianobarManager.OutputReceived -= PianobarManager_OutputReceived;
base.OnFormClosing(e);
}
#endregion
} }
} }