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(); private PianobarEventMonitor? _eventMonitor; private bool _receivedStdOut; private bool _receivedStdErr; private readonly string _ioLogPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PianobarHelper", "pianobar-io.log"); 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, string? eventFilePath = null) { if (IsConnected) throw new InvalidOperationException("Already connected to Pianobar."); _receivedStdOut = false; _receivedStdErr = false; // Check if pianobar is already running (check both with and without .exe) var existingProcesses = Process.GetProcessesByName("pianobar"); if (existingProcesses.Length == 0) { // Also try without extension string processName = Path.GetFileNameWithoutExtension(pianobarPath); existingProcesses = Process.GetProcessesByName(processName); } if (existingProcesses.Length > 0) { foreach (var proc in existingProcesses) proc.Dispose(); throw new InvalidOperationException("Pianobar is already running. Please close existing instances before connecting."); } // Validate the pianobar path exists string fullPath = pianobarPath; // If it's just a filename or not found, try to find it in PATH if (!File.Exists(fullPath)) { string? foundPath = FindInPath(pianobarPath); if (!string.IsNullOrEmpty(foundPath)) { fullPath = foundPath; } else { throw new FileNotFoundException( $"Pianobar executable not found.\n\n" + $"Path checked: {pianobarPath}\n" + $"Full path: {Path.GetFullPath(pianobarPath)}\n\n" + $"Please verify:\n" + $"1. The path in Settings is correct\n" + $"2. Pianobar is installed\n" + $"3. The file exists at the specified location"); } } try { var startInfo = new ProcessStartInfo { FileName = fullPath, UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, // Run in background without terminal window WindowStyle = ProcessWindowStyle.Hidden, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, WorkingDirectory = Path.GetDirectoryName(fullPath) ?? Environment.CurrentDirectory }; 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; bool started = _process.Start(); if (!started) { throw new InvalidOperationException("Process.Start() returned false. The process could not be started."); } // Verify the process actually started if (_process.HasExited) { throw new InvalidOperationException($"Process started but immediately exited with code: {_process.ExitCode}"); } _inputWriter = _process.StandardInput; _process.BeginOutputReadLine(); _process.BeginErrorReadLine(); IsConnected = true; // Start monitoring event file if provided if (!string.IsNullOrEmpty(eventFilePath)) { _eventMonitor = new PianobarEventMonitor(); _eventMonitor.EventReceived += OnEventReceived; _eventMonitor.StartMonitoring(eventFilePath); } RaiseEvent($"Pianobar started successfully (PID: {_process.Id})"); RaiseEvent($"I/O log: {_ioLogPath}"); } catch (Exception ex) { Cleanup(); throw new InvalidOperationException($"Failed to start Pianobar: {ex.Message}", ex); } } private string? FindInPath(string command) { // Check if the command is available in PATH var pathEnv = Environment.GetEnvironmentVariable("PATH"); if (string.IsNullOrEmpty(pathEnv)) return null; var paths = pathEnv.Split(Path.PathSeparator); var extensions = new[] { ".exe", ".cmd", ".bat", "" }; // Remove .exe if already present in command string baseCommand = Path.GetFileNameWithoutExtension(command); string commandExt = Path.GetExtension(command); foreach (var path in paths) { try { if (!Directory.Exists(path)) continue; foreach (var ext in extensions) { // If command already has extension, use it as-is if (!string.IsNullOrEmpty(commandExt)) { var fullPath = Path.Combine(path, command); if (File.Exists(fullPath)) return fullPath; } else { var fullPath = Path.Combine(path, baseCommand + ext); if (File.Exists(fullPath)) return fullPath; } } } catch { // Skip paths that cause errors continue; } } return null; } private bool IsCommandInPath(string command) { return !string.IsNullOrEmpty(FindInPath(command)); } public void Disconnect() { if (!IsConnected) return; Cleanup(); IsConnected = false; RaiseEvent("Disconnected from Pianobar (process detached)"); } public void DisconnectAndKill() { if (!IsConnected) return; try { if (_process != null && !_process.HasExited) { int pid = _process.Id; _process.Kill(); _process.WaitForExit(5000); // Wait up to 5 seconds for graceful exit RaiseEvent($"Pianobar process killed (PID: {pid})"); } } catch (Exception ex) { RaiseEvent($"Error killing process: {ex.Message}"); } finally { Cleanup(); IsConnected = false; } } 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; if (!_receivedStdOut) { _receivedStdOut = true; RaiseEvent("Received output on STDOUT"); } LogIoLine("STDOUT", line); OutputReceived?.Invoke(this, line); ParseOutput(line); } private void Process_ErrorDataReceived(object? sender, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { if (!_receivedStdErr) { _receivedStdErr = true; RaiseEvent("Received output on STDERR"); } LogIoLine("STDERR", e.Data); OutputReceived?.Invoke(this, e.Data); ParseOutput(e.Data); } } private void Process_Exited(object? sender, EventArgs e) { if (!_receivedStdOut) RaiseEvent("No STDOUT data received before process exit"); IsConnected = false; RaiseEvent("Pianobar process exited"); } private void LogIoLine(string source, string line) { try { var logDir = Path.GetDirectoryName(_ioLogPath); if (!string.IsNullOrEmpty(logDir)) Directory.CreateDirectory(logDir); File.AppendAllText(_ioLogPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{source}] {line}{Environment.NewLine}"); } catch { // Ignore logging errors } } 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 OnEventReceived(object? sender, PianobarEventData eventData) { // Update metadata from event file if (!string.IsNullOrEmpty(eventData.Title)) CurrentSong = eventData.Title; if (!string.IsNullOrEmpty(eventData.Artist)) CurrentArtist = eventData.Artist; if (!string.IsNullOrEmpty(eventData.Album)) CurrentAlbum = eventData.Album; if (!string.IsNullOrEmpty(eventData.CoverArt)) CurrentAlbumArtUrl = eventData.CoverArt; if (!string.IsNullOrEmpty(eventData.StationName)) { CurrentStation = eventData.StationName; StationChanged?.Invoke(this, CurrentStation); } // Raise event based on event type if (!string.IsNullOrEmpty(eventData.Event)) { switch (eventData.Event.ToLower()) { case "songstart": IsPlaying = true; TriggerSongChanged(); break; case "songfinish": break; case "songlove": RaiseEvent("Song Loved"); break; case "songban": RaiseEvent("Song Banned"); break; default: RaiseEvent($"Event: {eventData.Event}"); break; } } } private void RaiseEvent(string eventText) { EventOccurred?.Invoke(this, new PianobarEventArgs { EventText = eventText }); } private void Cleanup() { try { // Stop event monitoring if (_eventMonitor != null) { _eventMonitor.EventReceived -= OnEventReceived; _eventMonitor.Dispose(); _eventMonitor = null; } 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; } }