Files
Painobar-helper/PianobarManager.cs
T
2026-08-11 22:42:01 -04:00

562 lines
19 KiB
C#

using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
namespace PainoBar_Helper
{
/// <summary>
/// Manages the Pianobar process lifecycle and communication
/// </summary>
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<SongChangedEventArgs>? SongChanged;
public event EventHandler<string>? StationChanged;
public event EventHandler<PianobarEventArgs>? EventOccurred;
public event EventHandler? Connected;
public event EventHandler? Disconnected;
public event EventHandler<string>? 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;
}
}