[. Net and raspberry pie] Mini API encapsulation of MPD

Time:2022-3-21

In the previous hydrology, on the one hand, Lao Zhou introduced to you that you can access MPD service through TCP connection; On the other hand, it also briefly demonstrates ASP Net core’s Mini API. In this article, Lao Zhou will briefly talk about how to use mini API to encapsulate the access to MPD server. The content is for reference only. Maybe you will think of a better scheme.

You may ask: Lao Zhou, you lazy B, why did you wait so long to write this article after writing it last time? To be honest, I have a problem… The problem mainly lies in the “add” command.

The function of this command is to add a track to the current playlist (whether you load the previously saved list or not, in short, the playlist currently in use). Its format is:

add 

Remember the previous content? When configuring MPD, we will specify a directory for placing music files. Therefore, this audio URL generally uses a relative path, that is, the relative path to the music directory.

For example, your configured music directory is / home / pi / MPD / music, and then you put a subdirectory in the music directory called “Zhuangji 2021 new album”, which contains three files, with the structure roughly as follows:

Force 2021 new album
        |--A thousand year's costume compels the soul wav
        |--Pretend every day wav
        |--People who pretend to be forced are really helpless wav

That is, the full path of “Millennium costume. Wav” is / home / pi / MPD / music / Costume 2021 new album / Millennium costume WAV, but when you use the add command, you can only use the relative path, relative to the music directory.

Add "2021 new album / Millennium costume soul. Wav"

It’s best to put double quotes on the URL, because the probability of spaces in the path is high.

So, what’s the problem Lao Zhou encountered? Because the add command will refer to the audio file path, Chinese characters cannot be avoided in the text. At this point, you may understand that, yes, the old headache – text coding problem. If there are Chinese characters, you can’t use ASCII coding, but you can’t use UTF-8 coding explicitly. After many attempts, you still report an error.

Finally, Lao Zhou found a perfect solution – using encoding directly When the system is running, make the code consistent with default. Unexpectedly, this move solved all the problems and stopped reporting mistakes. Sure enough, the default is the most.

 

—————————————————————————————————-

Now that the problem has been solved, this hydrology can be written.

In order to facilitate operation, we might as well encapsulate a class separately, which is dedicated to communicating with the MPD service process. Now I’ll release the code of the whole class first, and then Lao Zhou will talk about the core part.

namespace MpcApi
{
using System;
using System.IO;
using System.Net;
using System.Collections.ObjectModel;
using System.Net.Sockets;
using static System.Text.Encoding;
using System.Text;
internal class MPDTCPClient : IDisposable
{
const string LOCAL_ HOST = "localhost";  //  Local address
const int LOCAL_ PORT = 6600;            //  Default port
TcpClient _client;
/// 
///Constructor
/// 
public MPDTCPClient()
{
_client = new TcpClient(LOCAL_HOST, LOCAL_PORT);
//Judge whether the MPD server responds
using StreamReader sr = new StreamReader(
stream: _client.GetStream(),
encoding: UTF8,
leaveOpen: true
);
string resp = sr.ReadLine();
if (resp == null || !resp.StartsWith("OK MPD"))
{
Throw new exception ("the server did not respond correctly");
}
}
public void Dispose()
{
_client?.Close();
}
private TextReader SendCommand(string cmd)
{
StreamWriter wr = new(
stream: _client.GetStream(),
encoding: Default,
leaveOpen: true);
wr. NewLine = "\n";  // Line breaks avoid "\ R \ n"
//Write command
wr.WriteLine(cmd);
wr.Flush();
wr.Dispose();
//Read response
StreamReader sr = new StreamReader(
stream: _client.GetStream(),
encoding: Default,
leaveOpen: true);
return sr;  // Leave it to other methods for further treatment
}
#Region the following methods are public members
/*
*Package it for convenience
*/
/// 
///Get available commands
/// 
public async Task> GetAvalidCommands()
{
List files = new();
using TextReader reader = SendCommand("commands");
string msg = await reader.ReadLineAsync();
while (msg != null && msg != "OK")
{
files.Add(msg);
msg = await reader.ReadLineAsync();
}
return new ReadOnlyCollection(files);
}
/// 
///Get a list of all songs
/// 
public async Task> GetAllSongs()
{
List list = new();
using TextReader reader = SendCommand("listall");
string line = await reader.ReadLineAsync();
while(line != null && line != "OK")
{
//Here we only need files, not directories
if (line.StartsWith("file:"))
{
list.Add(line);
}
line = await reader.ReadLineAsync();
}
return new ReadOnlyCollection(list);
}
/// 
///Play (specified track)
/// 
///Omitted track number, - 1
///True: successful; Otherwise fail s
public async Task Play(int n = -1)
{
string c = "play";
if(n >= 0)
{
c += $" {n}";
}
using TextReader reader = SendCommand(c);
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Pause
/// 
/// 
public async Task Pause()
{
using TextReader reader = SendCommand("pause");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Next song
/// 
/// 
public async Task Next()
{
using TextReader reader = SendCommand("next");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Last song
/// 
/// 
public async Task Previous()
{
using TextReader reader = SendCommand("previous");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Stop playing
/// 
/// 
public async Task Stop()
{
using TextReader reader = SendCommand("stop");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Set volume
/// 
///Volume value, which can be positive or negative
/// 
public async Task SetVolume(string v)
{
string c = $"volume {v}";
using TextReader reader = SendCommand(c);
if(await reader.ReadLineAsync() == "OK")
{
return true;
}
return false;
}
/// 
///Show tracks in playlist
/// 
/// 
public async Task> ShowPlaylist()
{
string c = "playlist";
using TextReader reader = SendCommand(c);
string msg = await reader.ReadLineAsync();
List items = new();
while(msg != null && msg != "OK")
{
items.Add(msg);
msg = await reader.ReadLineAsync();
}
return new ReadOnlyCollection(items);
}
/// 
///Clear the currently playing list
/// 
/// 
public async Task ClearList()
{
using TextReader reader = SendCommand("clear");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Load previously saved playlists
/// 
///Name of playlist
/// 
/// 
public async Task LoadList(string lsname)
{
if (string.IsNullOrWhiteSpace(lsname))
Throw new exception ("invalid list name")// The list name must be valid
string c = $"load {lsname}";
using TextReader reader = SendCommand(c);
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Save the current playlist
/// 
///Name of the new list
/// 
public async Task SaveList(string newname)
{
if (string.IsNullOrWhiteSpace(newname))
Throw new exception ("invalid new list name");
string cmd = $"save {newname}";
using TextReader rd = SendCommand(cmd);
if (await rd.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Delete playlist
/// 
///Name of playlist to delete
/// 
public async Task DeleteList(string lsname)
{
if(string.IsNullOrWhiteSpace(lsname))
{
Throw new exception ("playlist name is a required parameter");
}
using TextReader reader = SendCommand($"rm {lsname}");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Add songs to the current playlist
/// 
///Song URL
/// 
/// 
public async Task AddToList(string url)
{
if (url == null)
Throw new exception ("invalid URL");
using TextReader rd = SendCommand($"add {url}");
if (await rd.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Get the track being played
/// 
/// 
public async Task> GetCurrent()
{
List results = new();
using TextReader rd = SendCommand("currentsong");
string line = await rd.ReadLineAsync();
while (line != null && line != "OK")
{
results.Add(line);
line = await rd.ReadLineAsync();
}
return new ReadOnlyCollection(results);
}
#endregion
}
}

My class does not implement all commands, but only packages commonly used commands. As long as you understand its principle, you can expand it yourself.

By the way, we have to correct one point here: Lao Zhou demonstrated in the previous article that TCP protocol accesses MPD and sends text directly. Since I demonstrated that there is only one listall command, I will send the listall command immediately after connecting to the MPD server, and then receive the server response. Last time Lao Zhou said: the server first replied with an OK + MPD version number, then sent the file list, and the last sentence was OK.

In fact, Lao Zhou made a mistake here. The first sentence OK + MPD version number replied by the MPD server is not in response to the listall command, but immediately after the client successfully establishes a TCP connection with it. Therefore, the file list replied by MPD to the listall Command + OK.

So, looking back at the class just now, in the constructor, I let the tcpclient object connect to the MPD service (the server is local).

//After new, the connect method will be called automatically to request a connection            
_client = new TcpClient(LOCAL_HOST, LOCAL_PORT);
//Judge whether the MPD server responds
using StreamReader sr = new StreamReader(
stream: _client.GetStream(),
encoding: UTF8,
leaveOpen: true
);
//Once the connection is successful, MPD will immediately reply you with "OK MPD"
//Just judge the beginning of "OK MPD". The version number can be ignored. We don't care here
string resp = sr.ReadLine();
if (resp == null || !resp.StartsWith("OK MPD"))
{
Throw new exception ("the server did not respond correctly");
}

Another core method is “sendcommand”. Its function is to send a command to the MPD server, and then return a textreader object, which can read the response message of the MPD server.

private TextReader SendCommand(string cmd)
{
StreamWriter wr = new(
stream: _client.GetStream(),
Encoding: default, // the default encoding can solve all kinds of worries
leaveOpen: true);
wr. NewLine = "\n";  // Line breaks avoid "\ R \ n"
//Write command
wr.WriteLine(cmd);
wr. Flush(); // Be sure to this sentence, or it won't be sent
wr.Dispose();
//Read response
StreamReader sr = new StreamReader(
stream: _client.GetStream(),
Encoding: default, // default encoding
leaveOpen: true);
return sr;  // Leave it to other methods for further treatment
}

Then, various control methods call this method, which is difficult to trust with the MPD server, and are encapsulated and disclosed to the public.

/// 
///Get a list of all songs
/// 
public async Task> GetAllSongs()
{
List list = new();
using TextReader reader = SendCommand("listall");
string line = await reader.ReadLineAsync();
while(line != null && line != "OK")
{
//Here we only need files, not directories
if (line.StartsWith("file:"))
{
list.Add(line);
}
line = await reader.ReadLineAsync();
}
return new ReadOnlyCollection(list);
}
/// 
///Play (specified track)
/// 
///Omitted track number, - 1
///True: successful; Otherwise fail s
public async Task Play(int n = -1)
{
string c = "play";
if(n >= 0)
{
c += $" {n}";
}
using TextReader reader = SendCommand(c);
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
/// 
///Pause
/// 
/// 
public async Task Pause()
{
using TextReader reader = SendCommand("pause");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
…………

 

This mpdtcpclient encapsulation class establishes a connection when instantiating and closes the connection when releasing / cleaning. Then we register this class as a dependency injection service, and it is a short-term instance mode (instantiated every time we inject and released when we run out), which can avoid the long-term occupation of TCP connections and environmental pollution.

var builder = WebApplication.CreateBuilder(args);
//Add service
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddTransient();
builder.WebHost.UseUrls("http://*:888", "http://*:886");
var app = builder.Build();

 

Next, we can use the mapxxx extension method to define the mini API.

/**List all available commands**/
app.MapGet("/commands", async (MPDTCPClient client) =>
{
return await client.GetAvalidCommands();
});
/**List all songs**/
app.MapGet("/listall", async (MPDTCPClient client) =>
{
return await client.GetAllSongs();
});
/**Lists tracks in a playlist**/
app.MapGet("/lsplaylist", async (MPDTCPClient client) =>
{
return await client.ShowPlaylist();
});
/**Add to current playlist*/
app.MapPost("/add", async (string url, MPDTCPClient cl) =>
{
var res = await cl.AddToList(url);
return res ? Results.Ok() : Results.StatusCode(500);
});
/**Play**/
app.MapGet("/play", async (MPDTCPClient cl) =>
{
bool res = await cl.Play();
return res ? Results.Ok() : Results.StatusCode(500);
});
/**Pause**/
app.MapGet("/pause", async (MPDTCPClient client) =>
{
bool res = await client.Pause();
return res ? Results.Ok() : Results.StatusCode(500);
});
/**Stop playing**/
app.MapGet("/stop", async (MPDTCPClient client) =>
{
bool r = await client.Stop();
return r ? Results.Ok() : Results.StatusCode(500);
});
/**Last song**/
app.MapGet("/prev", async (MPDTCPClient cl) =>
{
bool procres = await cl.Previous();
if (procres)
return Results.Ok();
return Results.StatusCode(500);
});
/**Next song**/
app.MapGet("/next", async (MPDTCPClient client) =>
{
return (await client.Next()) ? Results.Ok() : Results.StatusCode(500);
});
/**Set volume**/
app.MapPost("/setvol", async (string vol, MPDTCPClient client) =>
{
bool res = await client.SetVolume(vol);
return res ? Results.Ok() : Results.StatusCode(500);
});
/**Clear the current playlist**/
app.MapGet("/clear", async (MPDTCPClient cl) =>
{
return (await cl.ClearList()) ? Results.Ok() : Results.StatusCode(500);
});
/**Load specified list**/
app.MapPost("/loadlist", async (string lsname, MPDTCPClient client) =>
{
bool r = await client.LoadList(lsname);
if (r)
return Results.Ok();
return Results.StatusCode(500);
});
/**Delete playlist**/
app.MapGet("/rmlist", async (string lsname, MPDTCPClient cl) =>
{
bool r = await cl.DeleteList(lsname);
return r ? Results.Ok() : Results.StatusCode(500);
});
/**Save current list**/
app.MapPost("/savelist", async (string listname, MPDTCPClient cl) =>
{
bool res = await cl.SaveList(listname);
return res ? Results.Ok() : Results.StatusCode(500);
});

The basic routine of this API is: if it is executed successfully, it returns 200 (OK); If the execution fails, 500 is returned.

The second parameter of mapxxx method is a [universal] delegate object. Note that when defining a delegate, you need a parameter of mpdtcpclient type, which will automatically obtain the object reference injected by dependency.

In general, you can supplement the encapsulation of other MPD commands as needed.

 

With the encapsulation of this API, the implementation of MPD client is much more flexible. You can make mobile app, web app or desktop program. Anyway, you can do whatever you like. No matter what you use as the client program, just call these web APIs.

Finally, take a few APIs to test.

First test the API of all commands listed in the following table.

 

The returned results are as follows:

[
"command: add",
"command: addid",
"command: addtagid",
"command: albumart",
"command: binarylimit",
"command: channels",
"command: clear",
"command: clearerror",
"command: cleartagid",
"command: close",
"command: commands",
"command: config",
"command: consume",
"command: count",
"command: crossfade",
"command: currentsong",
"command: decoders",
"command: delete",
"command: deleteid",
"command: delpartition",
"command: disableoutput",
"command: enableoutput",
"command: find",
"command: findadd",
"command: getfingerprint",
"command: idle",
"command: kill",
"command: list",
"command: listall",
"command: listallinfo",
"command: listfiles",
"command: listmounts",
"command: listpartitions",
"command: listplaylist",
"command: listplaylistinfo",
"command: listplaylists",
"command: load",
"command: lsinfo",
"command: mixrampdb",
"command: mixrampdelay",
"command: mount",
"command: move",
"command: moveid",
"command: moveoutput",
"command: newpartition",
"command: next",
"command: notcommands",
"command: outputs",
"command: outputset",
"command: partition",
"command: password",
"command: pause",
"command: ping",
"command: play",
"command: playid",
"command: playlist",
"command: playlistadd",
"command: playlistclear",
"command: playlistdelete",
"command: playlistfind",
"command: playlistid",
"command: playlistinfo",
"command: playlistmove",
"command: playlistsearch",
"command: plchanges",
"command: plchangesposid",
"command: previous",
"command: prio",
"command: prioid",
"command: random",
"command: rangeid",
"command: readcomments",
"command: readmessages",
"command: readpicture",
"command: rename",
"command: repeat",
"command: replay_gain_mode",
"command: replay_gain_status",
"command: rescan",
"command: rm",
"command: save",
"command: search",
"command: searchadd",
"command: searchaddpl",
"command: seek",
"command: seekcur",
"command: seekid",
"command: sendmessage",
"command: setvol",
"command: shuffle",
"command: single",
"command: stats",
"command: status",
"command: sticker",
"command: stop",
"command: subscribe",
"command: swap",
"command: swapid",
"command: tagtypes",
"command: toggleoutput",
"command: unmount",
"command: unsubscribe",
"command: update",
"command: urlhandlers",
"command: volume"
]

 

Test the listall command again.

 

 

Add a song to the current playlist. Note: the file name returned by MPD service starts with “file:”. When we pass the add command, we don’t need “file:”, just use the relative path (double quotation marks are recommended).

 

 

Test the playlist interface again and list the tracks in the current playlist.

 

The playlist returned is as follows:

[
"0: File: Huadie / 1 / Zhuo Yiting vs Zhou Weijie - Huadie. Wav",
"1: File: my Chinese heart / Zhang Mingmin - descendants of the dragon. Wav",
"2: File: Butterfly / 1 / Zhuo Yiting - full moon. Wav"
]

The number in front of “file:” is the position of the track in the playlist, calculated from 0. In this way, when using the play command, you can use this number to specify the track to play, such as the second track (position 1).

However, the play API written by Lao Zhou just now has no parameters. The whole list is played by default. We can change it.

app.MapGet("/play", async (int? pos, MPDTCPClient cl) =>
{
bool res = await cl.Play(pos ?? -1);
return res ? Results.Ok() : Results.StatusCode(500);
});

If the POS parameter is – 1, it means that the whole list is played from the beginning.

 

Now, you can call and play the second song.

 

 

Well, that’s all for today’s article. In advance, in the next hydrology, let’s play with LED color light strips.