Protocol

This module implements MPD’s protocol in terms of case classes and conversions from/to MPD commands and responses.

It’s only dependency is shapeless.

Almost all commands and its corresponding answers are implemented. Deprecated commands and some that I couldn’t get to work are missing.

Structuring

Case classes for various data types are in package mpc4s.protocol. Specific MPD command and response types in packages mpc4s.protocol.commands and mpc4s.protocol.answer, respectively. The package mpc4s.protocol.codecs contains the conversion from/to MPD protocol.

Examples

Commands

For example, to parse a find command:

import mpc4s.protocol._, mpc4s.protocol.codec._
// import mpc4s.protocol._
// import mpc4s.protocol.codec._

val cc = CommandCodec.defaultCodec
// cc: mpc4s.protocol.codec.LineCodec[mpc4s.protocol.Command] = mpc4s.protocol.codec.LineCodec$$anon$3@6f85ee02

val cmd = cc.parseValue("find album Echoes")
// cmd: mpc4s.protocol.codec.Result[mpc4s.protocol.Command] = Right(Find(Filter(ListMap(Map(TagFilter(Album) -> Echoes))),None,None))

cc.write(cmd.right.get)
// res0: mpc4s.protocol.codec.Result[String] = Right(find album Echoes)

The CommandCodec.defaultCodec can parse most mpd commands. In the example above the command was parsed into an object of class Find. Every command has its codec defined in its companion object. And all of them are then pulled together in CommandCodec.

Responses

This example parses the response to the status command:

import mpc4s.protocol.answer._
// import mpc4s.protocol.answer._

val mpdResponse = """volume: 90
repeat: 0
random: 0
single: 0
consume: 0
playlist: 2
playlistlength: 1
mixrampdb: 0.000000
state: play
song: 0
songid: 1
time: 36:780
elapsed: 36.223
bitrate: 457
duration: 780.000
audio: 44100:16:2
"""
// mpdResponse: String =
// "volume: 90
// repeat: 0
// random: 0
// single: 0
// consume: 0
// playlist: 2
// playlistlength: 1
// mixrampdb: 0.000000
// state: play
// song: 0
// songid: 1
// time: 36:780
// elapsed: 36.223
// bitrate: 457
// duration: 780.000
// audio: 44100:16:2
// "

val codec = StatusAnswer.codec
// codec: mpc4s.protocol.codec.LineCodec[mpc4s.protocol.answer.StatusAnswer] = mpc4s.protocol.codec.LineCodec$$anon$2@5ebf776c

val answer = codec.parseValue(mpdResponse)
// answer: mpc4s.protocol.codec.Result[mpc4s.protocol.answer.StatusAnswer] = Right(StatusAnswer(90,false,false,Off,false,2,1,Play,Some(0),Some(Id(1)),None,None,Some(Range(36,780)),Some(36.223),Some(780.0),Some(457),None,Some(0.0),Some(AudioFormat(44100,16,2)),Some(),Some()))

codec.write(answer.right.get)
// res1: mpc4s.protocol.codec.Result[String] =
// Right(volume: 90
// repeat: 0
// random: 0
// single: 0
// consume: 0
// playlist: 2
// playlistlength: 1
// state: play
// song: 0
// songid: 1
// time: 36:780
// elapsed: 36.223
// duration: 780.0
// bitrate: 457
// mixrampdb: 0.0
// audio: 44100:16:2
// )

Answer defines the payload returned from MPD, which are implemented in mpc4s.protocol.answer package (that also contains StatusAnswer). As with commands, the codec for an answer type is defined in its companion object.

There is also an Response trait which represents the response from MPD: either an error or some Answer. MPD responds with some payload (which may be empty) terminated by "OK\n", or with a so called ACK response. A codec for a response can be looked up from implicit scope, if a codec for Answer is available:

import mpc4s.protocol._, mpc4s.protocol.answer._
// import mpc4s.protocol._
// import mpc4s.protocol.answer._

val codec = implicitly[LineCodec[Response[StatusAnswer]]]
// codec: mpc4s.protocol.codec.LineCodec[mpc4s.protocol.Response[mpc4s.protocol.answer.StatusAnswer]] = mpc4s.protocol.codec.LineCodec$$anon$1@535b4dbf

val mpdResponse = """volume: 90
repeat: 0
random: 0
single: 0
consume: 0
playlist: 2
playlistlength: 1
mixrampdb: 0.000000
state: play
song: 0
songid: 1
time: 36:780
elapsed: 36.223
bitrate: 457
duration: 780.000
audio: 44100:16:2
OK
"""
// mpdResponse: String =
// "volume: 90
// repeat: 0
// random: 0
// single: 0
// consume: 0
// playlist: 2
// playlistlength: 1
// mixrampdb: 0.000000
// state: play
// song: 0
// songid: 1
// time: 36:780
// elapsed: 36.223
// bitrate: 457
// duration: 780.000
// audio: 44100:16:2
// OK
// "

codec.parseValue(mpdResponse)
// res2: mpc4s.protocol.codec.Result[mpc4s.protocol.Response[mpc4s.protocol.answer.StatusAnswer]] = Right(MpdResult(StatusAnswer(90,false,false,Off,false,2,1,Play,Some(0),Some(Id(1)),None,None,Some(Range(36,780)),Some(36.223),Some(780.0),Some(457),None,Some(0.0),Some(AudioFormat(44100,16,2)),Some(),Some())))

val ack = "ACK [50@2] {play} file not found\n"
// ack: String =
// "ACK [50@2] {play} file not found
// "

codec.parseValue(ack)
// res3: mpc4s.protocol.codec.Result[mpc4s.protocol.Response[mpc4s.protocol.answer.StatusAnswer]] = Right(MpdError(Ack(FileNotFound,2,play,file not found)))

Selecting the correct response codec

The correct codec for a mpd response depends on the command. The commands specify their answer type using an implicit value of SelectAnswer. For example, the list command expects its response to be a ListAnswer. So it there is this line it the companion object of List:

  implicit val selectAnswer = SelectAnswer[List, ListAnswer]

This can be used to let the compiler select the correct codec for a response given the command:

import mpc4s.protocol._, mpc4s.protocol.commands._
// import mpc4s.protocol._
// import mpc4s.protocol.commands._

def chooseCodec[C <: Command, A <: Answer](cmd: C)(implicit s: SelectAnswer[C, A]) =
  s.codec
// chooseCodec: [C <: mpc4s.protocol.Command, A <: mpc4s.protocol.Answer](cmd: C)(implicit s: mpc4s.protocol.SelectAnswer[C,A])mpc4s.protocol.codec.LineCodec[mpc4s.protocol.Response[A]]

chooseCodec(Status)
// res4: mpc4s.protocol.codec.LineCodec[mpc4s.protocol.Response[mpc4s.protocol.answer.StatusAnswer]] = mpc4s.protocol.codec.LineCodec$$anon$1@4b9187de

All this is pulled together in a map in CommandCodec that can be used to select codecs at runtime. It is called ProtocolConfig which is a type alias:

type ProtocolConfig = Map[CommandName, CommandName.Config]

The CommandName.Config has everything needed to parse/write a command and its response. Using this map, a codec for all commands can be created. CommandCodec.defaultConfig is the default ProtocolConfig containing all commands from this module. And CommandCodec.defaultCodec is the codec created using CommandCodec.defaultConfig.

Using that, a response and command codec can be looked up at runtime:

import mpc4s.protocol.commands._, mpc4s.protocol.codec._
// import mpc4s.protocol.commands._
// import mpc4s.protocol.codec._

// Using type Command, simulating we do not know the concrete type
val cmd: Command = Status
// cmd: mpc4s.protocol.Command = Status

// Looking up a CommandName.Config object using the command name
val cfg = CommandCodec.defaultConfig(cmd.name)
// cfg: mpc4s.protocol.CommandName.Config = mpc4s.protocol.CommandName$Config$$anon$2@65c6a7a

cfg.commandCodec.parseValue("status")
// res7: mpc4s.protocol.codec.Result[cfg.Cmd] = Right(Status)

cfg.responseCodec.parseValue("""volume: 90
repeat: 0
random: 0
single: 0
consume: 0
playlist: 2
playlistlength: 1
mixrampdb: 0.000000
state: play
song: 0
songid: 1
time: 36:780
elapsed: 36.223
bitrate: 457
duration: 780.000
audio: 44100:16:2
OK
""")
// res8: mpc4s.protocol.codec.Result[mpc4s.protocol.Response[cfg.Ans]] = Right(MpdResult(StatusAnswer(90,false,false,Off,false,2,1,Play,Some(0),Some(Id(1)),None,None,Some(Range(36,780)),Some(36.223),Some(780.0),Some(457),None,Some(0.0),Some(AudioFormat(44100,16,2)),Some(),Some())))

As you can see all the concrete types are gone, since we only have that information at runtime.