I think I broke it
This commit is contained in:
+187
-10
@@ -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<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<string>? 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<string> 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<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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user