using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; namespace Libmpc { /// /// Keeps the connection to the MPD server and handels the most basic structure of the /// MPD protocol. The high level commands are handeled in the /// class. /// public class MpcConnection { /// /// Is fired when a connection to a MPD server is established. /// public event EventHandler Connected; /// /// Is fired when the connection to the MPD server is closed. /// public event EventHandler Disconnected; private static readonly string FIRST_LINE_PREFIX = "OK MPD "; private static readonly string OK = "OK"; private static readonly string ACK = "ACK"; private static readonly Regex ACK_REGEX = new Regex("^ACK \\[(?[0-9]*)@(?[0-9]*)] \\{(?[a-z]*)} (?.*)$"); private IPEndPoint _ipEndPoint; private TcpClient _tcpClient; private NetworkStream _networkStream; private StreamReader _reader; private StreamWriter _writer; private string _version; /// /// If the connection to the MPD is connected. /// public bool IsConnected { get { return (_tcpClient != null) && _tcpClient.Connected; } } /// /// The version of the MPD. /// public string Version { get { return _version; } } private bool _autoConnect = false; /// /// If a connection should be established when a command is to be /// executed in disconnected state. /// public bool AutoConnect { get{ return _autoConnect; } set { _autoConnect = value; } } /// /// Creates a new MpdConnection. /// /// The IPEndPoint of the MPD server. public MpcConnection(IPEndPoint server) { Server = server; } /// /// The IPEndPoint of the MPD server. /// /// When a conenction to a MPD server is already established. public IPEndPoint Server { get { return _ipEndPoint; } set { if (IsConnected) throw new AlreadyConnectedException(); _ipEndPoint = value; ClearConnectionFields(); } } /// /// Connects to the MPD server who's IPEndPoint was set in the Server property. /// /// If no IPEndPoint was set to the Server property. public void Connect() { if (_ipEndPoint == null) throw new InvalidOperationException("Server IPEndPoint not set."); if (IsConnected) throw new AlreadyConnectedException(); _tcpClient = new TcpClient(); var connection = _tcpClient.ConnectAsync(_ipEndPoint.Address, _ipEndPoint.Port); connection.Wait(); _networkStream = _tcpClient.GetStream(); _reader = new StreamReader(_networkStream, Encoding.UTF8); _writer = new StreamWriter(_networkStream, Encoding.UTF8); _writer.NewLine = "\n"; string firstLine = _reader.ReadLine(); if( !firstLine.StartsWith( FIRST_LINE_PREFIX ) ) { Disconnect(); throw new InvalidDataException("Response of mpd does not start with \"" + FIRST_LINE_PREFIX + "\"." ); } _version = firstLine.Substring(FIRST_LINE_PREFIX.Length); _writer.WriteLine(); _writer.Flush(); ReadResponse(); Connected?.Invoke(this, EventArgs.Empty); } /// /// Disconnects from the current MPD server. /// public void Disconnect() { if (_tcpClient == null) return; _networkStream.Dispose(); ClearConnectionFields(); Disconnected?.Invoke(this, EventArgs.Empty); } /// /// Executes a simple command without arguments on the MPD server and returns the response. /// /// The command to execute. /// The MPD server response parsed into a basic object. /// If the command contains a space of a newline charakter. public MpdResponse Exec(string command) { if (command == null) throw new ArgumentNullException("command"); if (command.Contains(" ")) throw new ArgumentException("command contains space"); if (command.Contains("\n")) throw new ArgumentException("command contains newline"); CheckConnected(); try { _writer.WriteLine(command); _writer.Flush(); return ReadResponse(); } catch (Exception) { try { Disconnect(); } catch (Exception) { } return null; // TODO: Create Null Object for MpdResponse } } /// /// Executes a MPD command with arguments on the MPD server. /// /// The command to execute. /// The arguments of the command. /// The MPD server response parsed into a basic object. /// If the command contains a space of a newline charakter. public MpdResponse Exec(string command, string[] argument) { if (command == null) throw new ArgumentNullException("command"); if (command.Contains(" ")) throw new ArgumentException("command contains space"); if (command.Contains("\n")) throw new ArgumentException("command contains newline"); if (argument == null) throw new ArgumentNullException("argument"); for (int i = 0; i < argument.Length; i++) { if (argument[i] == null) throw new ArgumentNullException("argument[" + i + "]"); if (argument[i].Contains("\n")) throw new ArgumentException("argument[" + i + "] contains newline"); } CheckConnected(); try { _writer.Write(command); foreach (string arg in argument) { _writer.Write(' '); WriteToken(arg); } _writer.WriteLine(); _writer.Flush(); return ReadResponse(); } catch (Exception) { try { Disconnect(); } catch (Exception) { } throw; } } private void CheckConnected() { if (!IsConnected) { if (_autoConnect) Connect(); else throw new NotConnectedException(); } } private void WriteToken(string token) { if (token.Contains(" ")) { _writer.Write("\""); foreach (char chr in token) if (chr == '"') _writer.Write("\\\""); else _writer.Write(chr); } else _writer.Write(token); } private MpdResponse ReadResponse() { List ret = new List(); string line = _reader.ReadLine(); while (!(line.Equals(OK) || line.StartsWith(ACK))) { ret.Add(line); line = _reader.ReadLine(); } if (line.Equals(OK)) return new MpdResponse(new ReadOnlyCollection(ret)); else { Match match = ACK_REGEX.Match(line); if (match.Groups.Count != 5) throw new InvalidDataException( "Error response not as expected" ); return new MpdResponse( int.Parse( match.Result("${code}") ), int.Parse( match.Result("${nr}") ), match.Result("${command}"), match.Result("${message}"), new ReadOnlyCollection(ret) ); } } private void ClearConnectionFields() { _tcpClient?.Dispose(); _networkStream?.Dispose(); _reader?.Dispose(); _writer?.Dispose(); _version = string.Empty; } } }