Serveur Music Player Daemon commandé par S.A.R.A.H.

Bonjour à tous,
Aujourd’hui, nous allons voir un peu, comment crée un plugin. Je vais ici vous présenter la « gestation » de mon plugin de contrôle de serveur MPD.
Le but est d’ici vous montrer la rédaction d’un plugin pour quelqu’un de relativement néophyte, même si quelques notions de codages sont toutefois indispensables.
Ainsi, difficile d’appréhender cela sans savoir ce qu’est une variable.  Je ne dis pas que si vous n’avez pas ces connaissances, vous n’arriverez à rien. Juste que ce sera plus difficile. J’ai écrit ce billet en essayant d’être le plus pédagogique possible, mais il est vu de ma fenêtre.  La méthode présentée n’est pas LA méthode absolue. Mais pour moi elle a fonctionné.
En annexe, vous trouverez également une petite partie sur la réalisation d’une modification du *.xml à la volée, pour pouvoir appeler les serveurs par leur nom.

  • Qu’est-ce qu’un serveur MPD ?

« MPD est un lecteur de musique utilisant une architecture client-serveur. Le serveur va agir en tant que daemon (ou service) et s’occuper de lire la musique sur l’ordinateur sur lequel il est installé et garder en mémoire la liste de lecture. Les clients mpd permettent de construire sa playlist, de la contrôler et de voir les morceaux joués. Il n’est pas obligatoire que le client soit lancé pour que mpd puisse continuer a fonctionner normalement. Il est aussi possible d’ouvrir plusieurs clients simultanément sur autant de machines qu’on le souhaite. Les avantages liés a un tel fonctionnement sont entre autre la légèreté du programme puisque seul le serveur doit être actif pour jouer la musique et la modularité des clients puisqu’ils sont indépendants. Il existe des dizaines de clients différents et ceci sur des plate-formes extrêmement variées; des outils en ligne de commande aux interfaces graphiques plus ou moins complètes en passant par les librairies permettant de piloter mpd via une page web ou les applications pour téléphone portable. » (Source : http://doc.ubuntu-fr.org/mpd)
  • Structure d’un plugin S.A.R.A.H.

Un plugin pour SARAH est constitué d’au moins 3 fichiers.

    •  Un fichier *.js (ici controlempd.js )
    •  Un fichier *.xml (ici controlempd.xml )
    •  Un fichier *.prop (ici controlempd.prop )

Le *.js est le cerveau du plugin. C’est lui qui dit quoi faire en réaction a tel « stimuli » .
Le *.xml contient les règles vocales. Quand le client SARAH entendra une phrase, il la comparera avec toutes les règles XML des plugins pour ensuite savoir quel action faire (généralement mettre à jour une variable que l’on utilisera dans le *.js).
Le *.prop sert à donner une possibilité de réglages à l’utilisateur.

Pour que tout fonctionne, il est important de tout appeler de la même façon.
Ici, le dossier du plugin sera « controlempd » et contiendra « controlempd.xml », « controlempd.prop » et « controlempd.js » ainsi qu’un
fichier « index.html » (qui contiendra une page web de documentation sur le plugin).
Dans notre cas, il y aura aussi un dossier « bin » (qui contiendra notre programme MPC pour gérer les requêtes MPD pour nous)

Définition de nos besoins

A la base, ce plugin était, pour moi, un « bricolage perso » que je souhaitais être adapter à MON installation. Mais comme beaucoup, on commence par un bricolage, puis on se dit que ça intéresserait des gens, (et on se retrouve à écrire des tuto sur l’écriture d’un plugin !) et finalement, on le publie.

Chez moi, la problématique était
« Comment gérer mon serveur MPD principal à la voix ? »
puis, cela s’est mué en
« Comment gérer MES serveurs MPD à la voix, et surtout comment faire pour que ce soit User-Friendly ? »

Pour cela, j’ai eu la démarche suivante. Je ne dis pas que c’est LA bonne démarche. Mais elle a fonctionné chez moi.

a) Faire un système intuitif

Et oui, une reconnaissance vocale, c’est bien, mais si je dois passer mon temps dans la FaQ, ça perd tout intérêt…
Alors je me suis imaginé parler à SARAH. Ainsi sont venues les commandes :

  •  Sarah, musique suivante
  •  Sarah, musique précédente
  •  Sarah, met en pause
  •  Sarah, reprise
  •  Sarah, monte le son
  •  Sarah, baisse le son

Et là, mine de rien, on a fait les ¾ du boulot concernant le *.xml (vous savez, celui qui permet de définir les commandes vocales !). On y reviendra après.

b) Permettre de renseigner les serveurs

Car on ne peut pas se permettre de mettre les serveurs en dur dans le *.js
En effet chacun possède sa propre config avec son nombre de serveur, dans différentes pièces (peut-être).
L’idéal serait que l’utilisateur rentre lui-même sa config. Et c’est là que le *.prop rentre en jeu ! On a donc besoin de savoir :

  • – l’adresse IP du serveur1
  • – le port du serveur1

Idem pour les serveurs 2, 3, 4, etc etc…

Pour optimiser le traitement, nous allons également demander à l’utilisateur combien de serveur il possède.
Et voici les champs de notre *.prop définis.

c) Faire communiquer SARAH avec les serveur MPD

Alors ici, je ne me suis pas foulé. A quoi cela sert-il de réinventer la roue ? Pour communiquer avec MPD, il existe un programme utilisable en ligne de commande : MPC.
Il est tout léger, donc je me permets de le fournir dans l’archive du plugin. L’avantage d’un programme en ligne de commande est que je peux l’appeler directement avec le script *.js

  • Création du plugin

Et voici, nous avons tout ce qu’il nous faut. A présent, il faut « mettre les mains dans le cambouis » et construire notre plugin

a) Création du fichier controlempd.xml

Ici, nous allons créer le fichier contenant les requêtes vocales que SARAH devra interpréter

1
2
3
<grammar version="1.0" xml:lang="fr-FR" mode="voice" root="rulecontroleMPD" xmlns="http://www.w3.org/2001/06/grammar" tag-format="semantics/1.0">
<rule id="rulecontroleMPD" scope="public">
<example>Sarah tu es cool</example>
<grammar version="1.0" xml:lang="fr-FR" mode="voice" root="rulecontroleMPD" xmlns="http://www.w3.org/2001/06/grammar" tag-format="semantics/1.0">
<rule id="rulecontroleMPD" scope="public">
<example>Sarah tu es cool</example>

Ici, je vous avouerais avoir fait un simple copier/coller en ayant remplacé le nom du plugin
Ici, « out.action.piece= » »; » initie une variable « piece » à vide.
L’intérêt est de pouvoir avoir une valeur (même NULL) dans « pièce » même si l’utilisateur n’en précise pas.

Ensuite, les requêtes :
Pour dire que la commande doit commencer par « SARAH »
Ici, 3 éléments importants :

  • One-of : signifie que l’utilisateur doit utiliser 1seul élément dans la liste
  • <item>musique suivante[…]</item> : commande vocale que l’utilisateur doit dire.
  • <tag>out.action.commande= »next »</tag> : passera la valeur de action.commande à « next » (pour une utilisation dans le *.js)

A ajouter en fin de fichier. « http://127.0.0.1:8080/sarah/controleMPD » indique le répertoire de votre plugin.

b) Création du fichier controlempd.prop

Ici, nous allons créer le fichier de configuration paramétrable par l’utilisateur.
Dans ce fichier, il renseignera le nombre de serveur ainsi que leurs caractéristiques.

Toujours pareil, il est recommandé de s’inspirer d’un des plugins de démo que JPE a mis en ligne dans le store.

1
2
3
4
5
6
{
"modules" : {
"controleMPD" : {
"description": "Contrôle de serveur MPD",
"version": "2.0",
...
{
"modules" : {
"controleMPD" : {
"description": "Contrôle de serveur MPD",
"version": "2.0",
...

A renseigner avec le nom de votre module, une rapide description et une version si vous souhaitez.

1
2
3
4
5
6
7
8
9
...
"Nb_serv": "4",
"Piece_1": "piece 1",
"serveur1": "xxx.xxx.xxx.xxx",
"port1": "6600",
[…….]
}
}
}
...
"Nb_serv": "4",
"Piece_1": "piece 1",
"serveur1": "xxx.xxx.xxx.xxx",
"port1": "6600",
[…….]
}
}
}

Ici, vous mettez vos variables utilisateur. Cela fonctionne ainsi :
‘’Nom affiché’’ : ‘’valeur par défaut, modifiable par l’utilisateur’’

 

Contrôle de serveur MPD

Contrôle de serveur MPD

Nota : L’argument « Piece_1 » ne nous servira pas ici.

c) Création du fichier controlempd.js

Là, par contre, accrochez-vous ! On à toutes les briques en place, il ne reste plus qu’à tout empiler comme il faut pour que ça tienne debout. Ce n’est pas le plus simple, mais ça roule pas mal avec l’habitude. On commence avec cette ligne

1
2
exports.action = function(data, callback, config, SARAH)
{
exports.action = function(data, callback, config, SARAH)
{

Elle est obligatoire. Elle permet de faire la liaison entre un peu tout ce qui concerne le plugin , SARAH et le fichier que l’on est en train d’écrire.

Une rapide explication :

  • « data » contient les variables passées par le *.xml (et donc l’URL) , accessibles sous la forme « data.mavariable »
  • « config » contient les variables passées par le *.prop, accessible de la même façon, sous la forme « config.mavariable »

Cette façon décrire relève de la programmation orientée objet. Je vous laisse chercher pour plus d’info. Ici, nous allons apprendre à nous en servir, pas comment ça marche.

1
2
3
4
5
6
// Verify config
config = config.modules.controleMPD;
var os = require('os');
var host = config.serveur1;
var port = config.port1;
var systeme = os.platform();
// Verify config
config = config.modules.controleMPD;
var os = require('os');
var host = config.serveur1;
var port = config.port1;
var systeme = os.platform();

Ici « // Verifyconfig » est un « commentaire ». Pour l’exécution, il est invisible. Mais pour vous, modeste programmeur, il vous aide à savoir ce que vous faite.

« var os = require(‘os’); » indique que l’on va utiliser la librairie « os ». Elle nous servira par la suite à reconnaitre le système sur lequel tourne SARAH.

« var host = config.serveur1; » sert à insérer le contenu de la variable « serveur1 » du fichier *.prop dans la variable « host » du *.js (ici, d’après l’image, il s’agit de « 192.168.1.15 »). On fait de même pour la variable « port » avec la ligne d’après.
On charge donc automatiquement les variables du serveur n°1. Ceci permet de le mettre par défaut. Ainsi, si on ne précise pas la pièce dans la commande, ce sera ce serveur qui sera commandé.

« var systeme = os.platform();» Ici, on appel la fonction « platform » de la libraire « os ». Cette fonction va nous retourner «linux » ou « win32 » selon que l’on soit sur Linux ou Windows. Grâce à cette simple variable, on pourra faire réagir différemment le plugin, selon que l’on soit sur Windows ou sur Linux (et par extension Raspberry !)

1
2
3
4
5
6
7
8
9
10
11
12
13
//gestion du multiroom
if (data.piece !="")
{
switch (data.piece)
{
case "2": //reprendre config.Piece_2
var host = config.serveur2;
var port = config.port2;
break;
case3:
[…….]
}
}
//gestion du multiroom
if (data.piece !="")
{
switch (data.piece)
{
case "2": //reprendre config.Piece_2
var host = config.serveur2;
var port = config.port2;
break;
case “3”:
[…….]
}
}

Ici on permet à l’utilisateur de spécifier un serveur à commander. Si dans son URL l’utilisateur indique « piece=2 », le script verra que pièce n’est pas vide (rappelez-vous, on l’a initié comme variable vide, dans le *.xml) et pourra réagir en conséquence. Ici, il récupère les infos correspondantes en relation avec le serveur (ici, le serveur n°2, renseigné par l’utilisateur dans le *.prop)

1
2
3
4
5
6
7
8
9
switch (systeme)
{
case "linux":
[…….]
break;
case "win32":
[…….]
break;
}
switch (systeme)
{
case "linux":
[…….]
break;
case "win32":
[…….]
break;
}

Ici on fait un switch suivant l’OS relevé. En effet, que l’on soit sur un Linux ou un Windows, on agit pas de la même façon vu qu’on va devoir appeler un programme externe pour nous aider (MPC). Sur Linux on appellera la commande « mpc », que l’on peut obtenir par un « apt-get install mpc », tandis que sur Windows, on passera par le programme fournit dans le dossier « bin » fournit avec le plugin. Cette façon de faire permet de n’écrire qu’un plugin portable sur les 2 plateformes.

1
2
3
4
5
6
7
8
9
(data.commande)
{
case "next":
var exec = require("child_process").exec;
var requete = ".\\plugins\\controleMPD\\bin\\mpc -h "+ host +" -p "+ port +" next"; exec(requete, function (error, stdout, stderr) {
});
break;
[…….]
}
(data.commande)
{
case "next":
var exec = require("child_process").exec;
var requete = ".\\plugins\\controleMPD\\bin\\mpc -h "+ host +" -p "+ port +" next"; exec(requete, function (error, stdout, stderr) {
});
break;
[…….]
}

Ici, nous faisons un switch d’après la valeur de la variable « commande ». Dans l’exemple, nous utiliserons l’ordre « next ».

«var exec = require(« child_process »).exec; » nous permet ici d’appeler un processus, et donc d’exécuter une commande pour nous.

« var requete = « .\\plugins\\controleMPD\\bin\\mpc -h « + host + » -p « + port + » next »; » nous permet de construire la requête que nous allons créer.
On va la décortiquer ensemble :

  • « « .\\plugins\\controleMPD\\bin\\mpc » : chemin de l’exécutable MPC sous Windows. Il faut garder à l’esprit que c’est SARAH qui exécute. On part donc de son emplacement. Sous Linux c’est bien plus simple. mpc étant une commande du système (après installation), il suffit d’écrire « mpc ».
  • -h « + host + »: On passe l’argument –h (pour host) avec la valeur de « host » qui est l’adresse du serveur MPD
  • -p « + port + «  : On passe l’argument –p(pour port) avec la valeur de « port » qui est le port du serveur MPD
  •  » next » : On donne la commande à exécuter par mpc.

On voit ici une subtilité du javascript. Pour insérer le contenu d’une variable dans une chaîne de caractères, il faut faire « chaine_de_caractere »+mavariable+ »suite_de_la_chaine ».

Voici les 2 constructions possibles :

Pour Windows :
var requete = « .\\plugins\\controleMPD\\bin\\mpc -h « + host + » -p « + port + » next »;

Pour Linux :
var requete = « mpc -h « + host + » -p « + port + » next »;

«exec(requete, function (error, stdout, stderr) { });» Enfin, on finit par exécuter la requête.

1
2
3
4
5
exports.action = function(data, callback, config, SARAH)
{
[…….]
callback({});
}
exports.action = function(data, callback, config, SARAH)
{
[…….]
callback({});
}

Tout à la fin du plugin il est important de mettre « callback({}) ; » afin d’avoir un retour pour SARAH.

  • Annexe : modification du *.xml à la volée

Pouvoir contrôler plusieurs serveurs MPD, c’est bien. Mais rappelez-vous, notre objectif c’est d’avoir un système User-Friendly. Et appeler mes serveurs «serveur 1 », « serveur 2 », etc, c’est tout sauf intuitif. J’aimerais dire « Sarah, musique suivante dans la chambre » ou « Sarah, monte le son dans le salon ».
Mais rappelez-vous, les règles vocales sont figées en dur dans le *.xml . Qu’à cela ne tienne, nous allons donc le modifier !

a) Modification du fichier *.prop

Pour cela, nous allons commencer par demander à l’utilisateur de renseigner le nom de chaque serveur dans le fichier *.prop
Ainsi, on se retrouve avec une variable de plus par serveur :

« Piece_1 »: « piece 1 »,

Ici, l’utilisateur devra indiquer le nom par lequel il appellera le serveur n°1.
Tant qu’on y est, afin d’optimiser le traitement, on va demander à l’utilisateur de renseigner combien de serveurs il souhaite commander :

« Nb_serv »: « 4 »,

b) Modification du fichier *.xml

Nous allons maintenant préparer le fichier *.xml pour la génération.
Premièrement, nous allons insérer les mots de liaisons (« dans le », « dans la », « dans l’ », « du », « de la », « de l’ » ).

1
2
3
4
5
6
<item repeat="0-1">
<one-of>
<item>Dans la</item>
[…….]
</one-of>
</item>
<item repeat="0-1">
<one-of>
<item>Dans la</item>
[…….]
</one-of>
</item>

Le <item repeat= »0-1″> permet de spécifier qu’on attend 0 ou 1 seul des éléments de la liste. Aucun si on ne précise pas la pièce ou 1 seul dans le cas d’une phrase naturelle telle que « Sarah, musique suivante dans la chambre »

Ensuite on prépare la zone où sera injectée la génération :

1
2
3
4
5
6
7
8
<!-- Automatic Generation § -->
<item repeat="0-1">
<one-of>
<item>Salon<tag>out.action.piece="1";</tag></item>
<item>chambre<tag>out.action.piece="2";</tag></item>
</one-of>
</item>
<!-- § Automatic Generation -->
<!-- Automatic Generation § -->
<item repeat="0-1">
<one-of>
<item>Salon<tag>out.action.piece="1";</tag></item>
<item>chambre<tag>out.action.piece="2";</tag></item>
</one-of>
</item>
<!-- § Automatic Generation -->

<!– Automatic Generation § –> sera recherché par le script. Cette chaine sert de « balise »
A l’intérieur on retrouve une structure courante, avec un <item repeat= »0-1″>.
On peut noter qu’ici o modifie la variable « out.action.piece », qui sera ensuite testée dans le script.

Enfin, on prépare une commande vocale pour demander la mise à jour des infos. Il y a surement un moyen d’automatiser cela vu que le plugin se recharge sur chaque modification du *.prop dans l’interface de SARAH, mais cette solution me semblait la plus simple a mettre en oeuvre.

Dans la section des commandes principales :

1
<item>Met a jour les serveurs de musiques<tag>out.action.commande="maj"</tag></item>
<item>Met a jour les serveurs de musiques<tag>out.action.commande="maj"</tag></item>

c) Modification du fichier *.js

C’est le fichier *.js qui se chargera de la modification du *.xml avec les variables contenues dans le *.prop.
Afin de recueillir les différents noms, et de les passer à une fonction de génération, je suis partit sur la base d’un tableau contenant n+1 éléments, organisé ainsi :

  • Indice 0 : config.Nb_serv => contient le nombre de serveurs à générer.
  • Indice 1 à n : config.Piece_X => contient le nom des serveurs.
1
2
3
4
5
6
7
8
9
10
11
case "maj":
var piece = "config.Piece_1";
var serveur = new Array();
serveur[0] = config.Nb_serv;
for(var i = 1 ; i <= config.Nb_serv ; i++)
{
piece = "config.Piece_"+i;
serveur[i] = eval(piece);
}
update(data.directory, serveur);
break;
case "maj":
var piece = "config.Piece_1";
var serveur = new Array();
serveur[0] = config.Nb_serv;
for(var i = 1 ; i <= config.Nb_serv ; i++)
{
piece = "config.Piece_"+i;
serveur[i] = eval(piece);
}
update(data.directory, serveur);
break;

Toujours un switch sur data.commande. On initie la variable piece avec le nom du serveur n°1. On initie un tableau serveur sans spécifier la taille (merci le javascript et l’allocation dynamique !)
On attribut config.Nb_serv à la case 0 du tableau, puis on le remplit. La fonction «eval(piece) » permet de nous retourner le contenu de pièce. Sans cette fonction, le tableau serait remplit par des « piece_1 », « piece_2 », etc.

Ensuite, on passe ce tableau comme élément de la fonction update, accompagné de l’élément data.directory qui permettra de se repérer dans l’arborescence (et donc de retrouver le fichier *.xml que l’on va modifier).

Voici la fonction que l’on va utiliser (fortement inspirée du plugin Allocine):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var update = function(directory, serv_MPD)
{
var fs = require('fs');
var file = directory + '/../plugins/controleMPD/controleMPD.xml';
var xml = fs.readFileSync(file,'utf8');
var nombre_serveur = serv_MPD[0];
var replace = '§ -->\n';
replace += ' <item repeat="0-1">\n';
replace += ' <one-of>\n';
for(var i = 1 ; i <= nombre_serveur ; i++)
{
var lieu = replace += ' <item>'+serv_MPD[i]+'<tag>out.action.piece="'+i+'";</tag></item>\n';
}
replace += ' </one-of>\n';
replace += ' </item>\n';
replace += ' <!-- §';
var regexp = new RegExp('§[^§]+§','gm');
var xml = xml.replace(regexp,replace);
fs.writeFileSync(file, xml, 'utf8');
}
var update = function(directory, serv_MPD)
{
var fs = require('fs');
var file = directory + '/../plugins/controleMPD/controleMPD.xml';
var xml = fs.readFileSync(file,'utf8');
var nombre_serveur = serv_MPD[0];
var replace = '§ -->\n';
replace += ' <item repeat="0-1">\n';
replace += ' <one-of>\n';
for(var i = 1 ; i <= nombre_serveur ; i++)
{
var lieu = replace += ' <item>'+serv_MPD[i]+'<tag>out.action.piece="'+i+'";</tag></item>\n';
}
replace += ' </one-of>\n';
replace += ' </item>\n';
replace += ' <!-- §';
var regexp = new RegExp('§[^§]+§','gm');
var xml = xml.replace(regexp,replace);
fs.writeFileSync(file, xml, 'utf8');
}

Ici, on utilise une boucle for pour parcourir le tableau passé en paramètre, en partant de l’élément d’indice 1, l’élément 0 nous fournissant la longueur du tableau. On utilise également une RegExp pour retrouver les lignes de tag dans le fichier *.xml.
Puis on écrit tout le texte (construit à partir des éléments) dans le fichier *.xml .

  • Remerciement

Je souhaiterais ici remercier toute la communauté mais aussi, et surtout, Jean-Philippe Encausse, le créateur de cette superbe invention qu’est S.A.R.A.H.
Ce document est en libre échange. Ainsi, si vous le trouvez adapté, n’hésitez pas à le distribuer, sous réserve de deux conditions :

  • – Citer son auteur
  • – Indiquer la source, si possible par un lien, ainsi que le site du projet (http://encausse.net/s-a-r-a-h au moment de l’écriture du document)

En espérant que vous vous amuserez autant que moi à faire tous les plugins qui vous passent par la tête !

Auteur: Damien G.

Laisser un commentaire