I think I broke it

This commit is contained in:
minster586
2026-08-11 23:41:48 -04:00
parent 02bd8ffd5e
commit df5303e0ef
2 changed files with 271 additions and 58 deletions
+187 -10
View File
@@ -20,6 +20,14 @@ namespace PainoBar_Helper
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"PianobarHelper", "PianobarHelper",
"pianobar-io.log"); "pianobar-io.log");
private readonly object _stationsLock = new object();
private readonly SortedDictionary<int, string> _knownStations = new();
private readonly object _stationPromptLock = new object();
private TaskCompletionSource<bool>? _stationPromptTcs;
private readonly object _inputLock = new object();
private readonly object _logLock = new object();
private readonly List<string> _ioLogLines = new();
private int? _pendingStationIndex;
public event EventHandler<SongChangedEventArgs>? SongChanged; public event EventHandler<SongChangedEventArgs>? SongChanged;
public event EventHandler<string>? StationChanged; public event EventHandler<string>? StationChanged;
@@ -67,6 +75,14 @@ namespace PainoBar_Helper
_receivedStdOut = false; _receivedStdOut = false;
_receivedStdErr = false; _receivedStdErr = false;
lock (_stationPromptLock)
{
_pendingStationIndex = null;
_stationPromptTcs = null;
}
ResetIoLog();
// Check if pianobar is already running (check both with and without .exe) // Check if pianobar is already running (check both with and without .exe)
var existingProcesses = Process.GetProcessesByName("pianobar"); var existingProcesses = Process.GetProcessesByName("pianobar");
if (existingProcesses.Length == 0) if (existingProcesses.Length == 0)
@@ -267,8 +283,7 @@ namespace PainoBar_Helper
try try
{ {
_inputWriter.WriteLine(command); WriteInputCommand(command.ToString());
_inputWriter.Flush();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -283,8 +298,7 @@ namespace PainoBar_Helper
try try
{ {
_inputWriter.WriteLine(command); WriteInputCommand(command);
_inputWriter.Flush();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -292,6 +306,22 @@ namespace PainoBar_Helper
} }
} }
private void WriteInputCommand(string command)
{
if (_inputWriter == null)
throw new InvalidOperationException("Standard input is not available.");
lock (_inputLock)
{
// Use LF explicitly to avoid CRLF handling issues with redirected stdin.
_inputWriter.Write(command);
_inputWriter.Write('\n');
_inputWriter.Flush();
}
LogIoLine("STDIN", command);
}
public void PlayPause() public void PlayPause()
{ {
SendCommand('p'); SendCommand('p');
@@ -310,12 +340,68 @@ namespace PainoBar_Helper
SendCommand('s'); SendCommand('s');
} }
public async Task SelectStationAsync(int stationIndex)
{
if (stationIndex < 0)
throw new ArgumentOutOfRangeException(nameof(stationIndex));
if (!IsConnected || _inputWriter == null)
throw new InvalidOperationException("Not connected to Pianobar.");
bool isInitialSelection = IsInitialStationSelection(stationIndex);
if (isInitialSelection)
{
// Startup flow: station list already printed by Pianobar, submit index directly.
SendCommand(stationIndex.ToString());
RaiseEvent($"Initial station selection submitted: {stationIndex}");
return;
}
// Station change flow: request station mode, then submit index quickly.
ShowStations();
await Task.Delay(150);
SendCommand(stationIndex.ToString());
RaiseEvent($"Station change submitted: {stationIndex}");
}
private bool IsInitialStationSelection(int stationIndex)
{
if (!string.IsNullOrEmpty(CurrentSong))
return false;
lock (_stationsLock)
{
return _knownStations.ContainsKey(stationIndex) && _knownStations.Count > 0;
}
}
public IReadOnlyList<(int Index, string Name)> GetKnownStationEntries()
{
lock (_stationsLock)
{
return _knownStations
.OrderBy(kvp => kvp.Key)
.Select(kvp => (kvp.Key, kvp.Value))
.ToList();
}
}
public IReadOnlyList<string> GetKnownStations()
{
lock (_stationsLock)
{
return _knownStations.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value).ToList();
}
}
private void Process_OutputDataReceived(object? sender, DataReceivedEventArgs e) private void Process_OutputDataReceived(object? sender, DataReceivedEventArgs e)
{ {
if (string.IsNullOrEmpty(e.Data)) if (string.IsNullOrEmpty(e.Data))
return; return;
string line = e.Data; string line = e.Data;
CaptureStationLine(line);
if (!_receivedStdOut) if (!_receivedStdOut)
{ {
_receivedStdOut = true; _receivedStdOut = true;
@@ -332,6 +418,7 @@ namespace PainoBar_Helper
{ {
if (!string.IsNullOrEmpty(e.Data)) if (!string.IsNullOrEmpty(e.Data))
{ {
CaptureStationLine(e.Data);
if (!_receivedStdErr) if (!_receivedStdErr)
{ {
_receivedStdErr = true; _receivedStdErr = true;
@@ -344,13 +431,83 @@ namespace PainoBar_Helper
} }
} }
private void Process_Exited(object? sender, EventArgs e) private void CaptureStationLine(string line)
{ {
if (!_receivedStdOut) var clean = StripAnsiCodes(line).Trim();
RaiseEvent("No STDOUT data received before process exit");
IsConnected = false; SignalStationPromptIfWaiting(clean);
RaiseEvent("Pianobar process exited");
if (clean.Contains("Get stations... Ok.", StringComparison.OrdinalIgnoreCase))
{
lock (_stationsLock)
{
_knownStations.Clear();
}
return;
}
var match = Regex.Match(clean, @"^\s*(\d+)\)\s*(?:[qQ]\s+)?(.+?)\s*$");
if (!match.Success)
return;
int index = int.Parse(match.Groups[1].Value);
string stationName = match.Groups[2].Value.Trim();
lock (_stationsLock)
{
_knownStations[index] = stationName;
}
}
private void SignalStationPromptIfWaiting(string cleanLine)
{
if (string.IsNullOrEmpty(cleanLine))
return;
bool isStationPrompt =
cleanLine.Contains("Select station:", StringComparison.OrdinalIgnoreCase) ||
cleanLine.StartsWith("[?]", StringComparison.Ordinal);
if (!isStationPrompt)
return;
int? pendingIndex = null;
TaskCompletionSource<bool>? tcs = null;
lock (_stationPromptLock)
{
pendingIndex = _pendingStationIndex;
_pendingStationIndex = null;
tcs = _stationPromptTcs;
_stationPromptTcs = null;
}
if (pendingIndex.HasValue)
{
SendCommand(pendingIndex.Value.ToString());
RaiseEvent($"Station change submitted: {pendingIndex.Value}");
}
tcs?.TrySetResult(true);
}
private void ResetIoLog()
{
try
{
var logDir = Path.GetDirectoryName(_ioLogPath);
if (!string.IsNullOrEmpty(logDir))
Directory.CreateDirectory(logDir);
lock (_logLock)
{
_ioLogLines.Clear();
File.WriteAllText(_ioLogPath, string.Empty);
}
}
catch
{
// Ignore logging errors
}
} }
private void LogIoLine(string source, string line) private void LogIoLine(string source, string line)
@@ -361,7 +518,13 @@ namespace PainoBar_Helper
if (!string.IsNullOrEmpty(logDir)) if (!string.IsNullOrEmpty(logDir))
Directory.CreateDirectory(logDir); Directory.CreateDirectory(logDir);
File.AppendAllText(_ioLogPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{source}] {line}{Environment.NewLine}"); string entry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{source}] {line}";
lock (_logLock)
{
_ioLogLines.Add(entry);
File.WriteAllText(_ioLogPath, string.Join(Environment.NewLine, _ioLogLines) + Environment.NewLine);
}
} }
catch catch
{ {
@@ -544,6 +707,20 @@ namespace PainoBar_Helper
{ {
Disconnect(); Disconnect();
} }
private static string StripAnsiCodes(string value)
{
return Regex.Replace(value, @"\x1B\[[0-9;]*[A-Za-z]", string.Empty);
}
private void Process_Exited(object? sender, EventArgs e)
{
if (!_receivedStdOut)
RaiseEvent("No STDOUT data received before process exit");
IsConnected = false;
RaiseEvent("Pianobar process exited");
}
} }
public class SongChangedEventArgs : EventArgs public class SongChangedEventArgs : EventArgs
+76 -40
View File
@@ -3,18 +3,25 @@ namespace PainoBar_Helper
public partial class StationForm : Form public partial class StationForm : Form
{ {
private readonly PianobarManager _pianobarManager; private readonly PianobarManager _pianobarManager;
private List<string> _stations; private List<StationItem> _stations;
private List<string> _filteredStations; private List<StationItem> _filteredStations;
private bool _stationsLoaded = false; private bool _stationsLoaded = false;
private bool _stationEntriesStarted = false; private bool _stationEntriesStarted = false;
private sealed class StationItem
{
public int Index { get; set; }
public string Name { get; set; } = string.Empty;
public override string ToString() => Name;
}
public StationForm(PianobarManager pianobarManager) public StationForm(PianobarManager pianobarManager)
{ {
InitializeComponent(); InitializeComponent();
_pianobarManager = pianobarManager; _pianobarManager = pianobarManager;
_stations = new List<string>(); _stations = new List<StationItem>();
_filteredStations = new List<string>(); _filteredStations = new List<StationItem>();
InitializeControls(); InitializeControls();
} }
@@ -30,6 +37,11 @@ namespace PainoBar_Helper
{ {
lstStations.DoubleClick += LstStations_DoubleClick; lstStations.DoubleClick += LstStations_DoubleClick;
} }
if (refresh_button != null)
{
refresh_button.Click += refresh_button_Click;
}
} }
protected override void OnShown(EventArgs e) protected override void OnShown(EventArgs e)
@@ -40,23 +52,39 @@ namespace PainoBar_Helper
LoadStations(); LoadStations();
} }
private void LoadStations() private void LoadStations(bool forceRefresh = false)
{ {
_stations.Clear();
_filteredStations.Clear(); _filteredStations.Clear();
_stationsLoaded = false; _stationsLoaded = false;
_stationEntriesStarted = false; _stationEntriesStarted = false;
// Subscribe to output to capture station list if (forceRefresh)
_stations.Clear();
var knownStations = _pianobarManager.GetKnownStationEntries();
if (knownStations.Count > 0)
{
_stations = knownStations
.Select(s => new StationItem { Index = s.Index, Name = s.Name })
.OrderBy(s => s.Index)
.ToList();
_stationEntriesStarted = true;
if (lstStations != null)
UpdateStationList();
}
_pianobarManager.OutputReceived -= PianobarManager_OutputReceived;
_pianobarManager.OutputReceived += PianobarManager_OutputReceived; _pianobarManager.OutputReceived += PianobarManager_OutputReceived;
// Send command to show stations // Only request station list on explicit refresh.
if (forceRefresh)
{
try try
{ {
_pianobarManager.ShowStations(); _pianobarManager.ShowStations();
// Show loading message if (lstStations != null && _stations.Count == 0)
if (lstStations != null)
{ {
lstStations.Items.Clear(); lstStations.Items.Clear();
lstStations.Items.Add("Loading stations..."); lstStations.Items.Add("Loading stations...");
@@ -68,6 +96,16 @@ namespace PainoBar_Helper
MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
} }
else
{
_stationsLoaded = true;
if (_stations.Count == 0 && lstStations != null)
{
lstStations.Items.Clear();
lstStations.Items.Add("No cached stations. Click Refresh stations.");
}
}
}
#endregion #endregion
@@ -79,16 +117,24 @@ namespace PainoBar_Helper
if (string.IsNullOrEmpty(line)) if (string.IsNullOrEmpty(line))
return; return;
// Parse station list from Pianobar output var match = System.Text.RegularExpressions.Regex.Match(line, @"^\s*(\d+)\)\s*(?:[qQ]\s+)?(.+?)\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) if (match.Success)
{ {
_stationEntriesStarted = true; _stationEntriesStarted = true;
int stationIndex = int.Parse(match.Groups[1].Value);
string stationName = match.Groups[2].Value.Trim(); string stationName = match.Groups[2].Value.Trim();
if (!_stations.Contains(stationName))
var existing = _stations.FirstOrDefault(s => s.Index == stationIndex);
if (existing == null)
{ {
_stations.Add(stationName); _stations.Add(new StationItem { Index = stationIndex, Name = stationName });
_stations = _stations.OrderBy(s => s.Index).ToList();
if (!IsDisposed && IsHandleCreated)
BeginInvoke(() => UpdateStationList());
}
else if (!string.Equals(existing.Name, stationName, StringComparison.Ordinal))
{
existing.Name = stationName;
if (!IsDisposed && IsHandleCreated) if (!IsDisposed && IsHandleCreated)
BeginInvoke(() => UpdateStationList()); BeginInvoke(() => UpdateStationList());
} }
@@ -96,7 +142,8 @@ namespace PainoBar_Helper
} }
// Detect end of station list only after we started receiving entries // Detect end of station list only after we started receiving entries
if (_stationEntriesStarted && line.Contains("Select station:", StringComparison.OrdinalIgnoreCase)) if (_stationEntriesStarted &&
(line.Contains("Select station:", StringComparison.OrdinalIgnoreCase) || line == "[?]"))
{ {
_stationsLoaded = true; _stationsLoaded = true;
_pianobarManager.OutputReceived -= PianobarManager_OutputReceived; _pianobarManager.OutputReceived -= PianobarManager_OutputReceived;
@@ -154,7 +201,7 @@ namespace PainoBar_Helper
else else
{ {
_filteredStations = _stations _filteredStations = _stations
.Where(s => s.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0) .Where(s => s.Name.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0)
.ToList(); .ToList();
} }
@@ -166,17 +213,17 @@ namespace PainoBar_Helper
#region Station Selection #region Station Selection
// Wire this to btnSelectStation.Click in the Designer // Wire this to btnSelectStation.Click in the Designer
public void btnSelectStation_Click(object? sender, EventArgs e) public async void btnSelectStation_Click(object? sender, EventArgs e)
{ {
SelectCurrentStation(); await SelectCurrentStationAsync();
} }
private void LstStations_DoubleClick(object? sender, EventArgs e) private async void LstStations_DoubleClick(object? sender, EventArgs e)
{ {
SelectCurrentStation(); await SelectCurrentStationAsync();
} }
private void SelectCurrentStation() private async Task SelectCurrentStationAsync()
{ {
if (lstStations == null) if (lstStations == null)
return; return;
@@ -188,21 +235,12 @@ namespace PainoBar_Helper
return; return;
} }
string selectedStation = lstStations.SelectedItem?.ToString() ?? string.Empty; if (lstStations.SelectedItem is not StationItem selectedStation)
if (string.IsNullOrEmpty(selectedStation))
return;
// Find the index in the original station list
int stationIndex = _stations.IndexOf(selectedStation);
if (stationIndex < 0)
return; return;
try try
{ {
// Send the station index to Pianobar await _pianobarManager.SelectStationAsync(selectedStation.Index);
// Pianobar expects the station number (0-based index)
_pianobarManager.SendCommand(stationIndex.ToString());
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
Close(); Close();
} }
@@ -213,6 +251,11 @@ namespace PainoBar_Helper
} }
} }
public void refresh_button_Click(object? sender, EventArgs e)
{
LoadStations(forceRefresh: true);
}
#endregion #endregion
#region Cancel #region Cancel
@@ -220,13 +263,6 @@ namespace PainoBar_Helper
// Wire this to btnCancelStation.Click in the Designer (or Cancel button) // Wire this to btnCancelStation.Click in the Designer (or Cancel button)
public void btnCancelStation_Click(object? sender, EventArgs e) public void btnCancelStation_Click(object? sender, EventArgs e)
{ {
// Send 'q' to cancel station selection in Pianobar
try
{
_pianobarManager.SendCommand('q');
}
catch { }
DialogResult = DialogResult.Cancel; DialogResult = DialogResult.Cancel;
Close(); Close();
} }