From df5303e0ef3f8b126e5e20887cc8b880efc72eb5 Mon Sep 17 00:00:00 2001 From: minster586 <43217359+minster586@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:41:48 -0400 Subject: [PATCH] I think I broke it --- PianobarManager.cs | 197 ++++++++++++++++++++++++++++++++++++++++++--- StationForm.cs | 132 +++++++++++++++++++----------- 2 files changed, 271 insertions(+), 58 deletions(-) diff --git a/PianobarManager.cs b/PianobarManager.cs index 1a2bad1..58c9423 100644 --- a/PianobarManager.cs +++ b/PianobarManager.cs @@ -20,6 +20,14 @@ namespace PainoBar_Helper Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PianobarHelper", "pianobar-io.log"); + private readonly object _stationsLock = new object(); + private readonly SortedDictionary _knownStations = new(); + private readonly object _stationPromptLock = new object(); + private TaskCompletionSource? _stationPromptTcs; + private readonly object _inputLock = new object(); + private readonly object _logLock = new object(); + private readonly List _ioLogLines = new(); + private int? _pendingStationIndex; public event EventHandler? SongChanged; public event EventHandler? StationChanged; @@ -67,6 +75,14 @@ namespace PainoBar_Helper _receivedStdOut = false; _receivedStdErr = false; + lock (_stationPromptLock) + { + _pendingStationIndex = null; + _stationPromptTcs = null; + } + + ResetIoLog(); + // Check if pianobar is already running (check both with and without .exe) var existingProcesses = Process.GetProcessesByName("pianobar"); if (existingProcesses.Length == 0) @@ -267,8 +283,7 @@ namespace PainoBar_Helper try { - _inputWriter.WriteLine(command); - _inputWriter.Flush(); + WriteInputCommand(command.ToString()); } catch (Exception ex) { @@ -283,8 +298,7 @@ namespace PainoBar_Helper try { - _inputWriter.WriteLine(command); - _inputWriter.Flush(); + WriteInputCommand(command); } 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() { SendCommand('p'); @@ -310,12 +340,68 @@ namespace PainoBar_Helper 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 GetKnownStations() + { + lock (_stationsLock) + { + return _knownStations.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value).ToList(); + } + } + private void Process_OutputDataReceived(object? sender, DataReceivedEventArgs e) { if (string.IsNullOrEmpty(e.Data)) return; string line = e.Data; + CaptureStationLine(line); if (!_receivedStdOut) { _receivedStdOut = true; @@ -332,6 +418,7 @@ namespace PainoBar_Helper { if (!string.IsNullOrEmpty(e.Data)) { + CaptureStationLine(e.Data); if (!_receivedStdErr) { _receivedStdErr = true; @@ -344,13 +431,83 @@ namespace PainoBar_Helper } } - private void Process_Exited(object? sender, EventArgs e) + private void CaptureStationLine(string line) { - if (!_receivedStdOut) - RaiseEvent("No STDOUT data received before process exit"); + var clean = StripAnsiCodes(line).Trim(); - IsConnected = false; - RaiseEvent("Pianobar process exited"); + SignalStationPromptIfWaiting(clean); + + 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? 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) @@ -361,7 +518,13 @@ namespace PainoBar_Helper if (!string.IsNullOrEmpty(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 { @@ -544,6 +707,20 @@ namespace PainoBar_Helper { 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 diff --git a/StationForm.cs b/StationForm.cs index 9025eea..93a34dc 100644 --- a/StationForm.cs +++ b/StationForm.cs @@ -3,18 +3,25 @@ namespace PainoBar_Helper public partial class StationForm : Form { private readonly PianobarManager _pianobarManager; - private List _stations; - private List _filteredStations; + private List _stations; + private List _filteredStations; private bool _stationsLoaded = 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) { InitializeComponent(); _pianobarManager = pianobarManager; - _stations = new List(); - _filteredStations = new List(); + _stations = new List(); + _filteredStations = new List(); InitializeControls(); } @@ -30,6 +37,11 @@ namespace PainoBar_Helper { lstStations.DoubleClick += LstStations_DoubleClick; } + + if (refresh_button != null) + { + refresh_button.Click += refresh_button_Click; + } } protected override void OnShown(EventArgs e) @@ -40,32 +52,58 @@ namespace PainoBar_Helper LoadStations(); } - private void LoadStations() + private void LoadStations(bool forceRefresh = false) { - _stations.Clear(); _filteredStations.Clear(); _stationsLoaded = 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; - // Send command to show stations - try + // Only request station list on explicit refresh. + if (forceRefresh) { - _pianobarManager.ShowStations(); - - // Show loading message - if (lstStations != null) + try { - lstStations.Items.Clear(); - lstStations.Items.Add("Loading stations..."); + _pianobarManager.ShowStations(); + + if (lstStations != null && _stations.Count == 0) + { + 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); } } - catch (Exception ex) + else { - MessageBox.Show($"Failed to load stations:\n{ex.Message}", "Error", - MessageBoxButtons.OK, MessageBoxIcon.Error); + _stationsLoaded = true; + if (_stations.Count == 0 && lstStations != null) + { + lstStations.Items.Clear(); + lstStations.Items.Add("No cached stations. Click Refresh stations."); + } } } @@ -79,16 +117,24 @@ namespace PainoBar_Helper if (string.IsNullOrEmpty(line)) return; - // Parse station list from Pianobar output - // Handles formats like: "0) Station", "*0) Station", " 12) Station" - var match = System.Text.RegularExpressions.Regex.Match(line, @"^\s*\*?\s*(\d+)\)\s*(.+)$"); + var match = System.Text.RegularExpressions.Regex.Match(line, @"^\s*(\d+)\)\s*(?:[qQ]\s+)?(.+?)\s*$"); if (match.Success) { _stationEntriesStarted = true; + int stationIndex = int.Parse(match.Groups[1].Value); 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) BeginInvoke(() => UpdateStationList()); } @@ -96,7 +142,8 @@ namespace PainoBar_Helper } // 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; _pianobarManager.OutputReceived -= PianobarManager_OutputReceived; @@ -154,7 +201,7 @@ namespace PainoBar_Helper else { _filteredStations = _stations - .Where(s => s.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0) + .Where(s => s.Name.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0) .ToList(); } @@ -166,17 +213,17 @@ namespace PainoBar_Helper #region Station Selection // 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) return; @@ -188,21 +235,12 @@ namespace PainoBar_Helper 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) + if (lstStations.SelectedItem is not StationItem selectedStation) return; try { - // Send the station index to Pianobar - // Pianobar expects the station number (0-based index) - _pianobarManager.SendCommand(stationIndex.ToString()); - + await _pianobarManager.SelectStationAsync(selectedStation.Index); DialogResult = DialogResult.OK; Close(); } @@ -213,6 +251,11 @@ namespace PainoBar_Helper } } + public void refresh_button_Click(object? sender, EventArgs e) + { + LoadStations(forceRefresh: true); + } + #endregion #region Cancel @@ -220,13 +263,6 @@ namespace PainoBar_Helper // 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(); }