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.