329 lines
10 KiB
C#
329 lines
10 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();
|
|
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
}
|