using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; namespace PainoBar_Helper { /// /// Manages the Pianobar process lifecycle and communication /// public class PianobarManager : IDisposable { private Process? _process; private StreamWriter? _inputWriter; private bool _isConnected; private readonly object _lockObject = new object(); public event EventHandler? SongChanged; public event EventHandler? StationChanged; public event EventHandler? EventOccurred; public event EventHandler? Connected; public event EventHandler? Disconnected; public event EventHandler? 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; } }