Mettre à jour la médiathèque de Kodi

Kodi est un lecteur multimédia libre. Je l’utilise à partir de LibreELEC sur un Raspberry Pi. Au moment d’écrire ces lignes, la version 9 (Leia) est la plus récente. J’ai configuré Kodi pour aller lire des fichiers vidéos sur un ordinateur sur mon réseau par SMB. Donc, il arrive que j’ajoute des fichiers sur ce disque et que je doive mettre à jour manuellement Kodi pour afficher ces nouveaux fichiers. Dans cet article nous allons voir comment provoquer la mise à jour automatiquement à partir d’une application codée avec C++Builder.

Un collègue au travail m’a parlé que Kodi possède un API qui permet d’effectuer plusieurs tâches. Il l’utilise lui-même dans un logiciel de sa conception. Il m’a dit que Kodi Leia avait brisé une des fonctionnalités qu’il utilise. L’API HTTP ne fonctionnait plus pour mettre à jour la médiathèque. Effectivement, sur le Wiki de Kodi on peux y lire le texte suivant.

HTTP (does not work in v18 Leia)

Pour remédier à cette situation il a décidé d’invoquer l’application curl. En général je n’aime pas que mes applications aient des dépendances externes. J’ai donc creusé un peu pour comprendre comment mieux faire cette action.

Tout d’abord avant de débuter il faut activer l’option Autoriser le contrôle à distance via HTTP.

Kodi: Autoriser le contrôle à distance via HTTP

Dans l’image j’ai encerclé où se trouve le port que vous allez devoir utiliser.

Avant d’utiliser mon propre code, je vais tester la fonctionnalité. Sur la page HOW-TO:Remotely update library on y trouve une commande curl que je vais utiliser dans une fenêtre de terminal.

curl --data-binary '{"jsonrpc": "2.0", "method": "VideoLibrary.Scan", "id": "mybash"}' -H 'content-type: application/json;' http://libreelec:8080/jsonrpc

Voici la réponse que j’obtiens.

{"id":"mybash","jsonrpc":"2.0","result":"OK"}

Cette commande fonctionne bien, donc on ne devrait pas avoir de problème à communiquer avec Kodi. Une chose intéressante que l’on peut voir est qu’elle utilise l’étiquette jsonrpc. En cherchant un peu on comprend que Kodi à commencer à migrer de l’API HTTP vers un API JSON-RPC. Voici ce que la documentation dit à ce sujet.

JSON-RPC is a HTTP- and/or raw TCP socket-based interface for communicating with Kodi. It replaces the deprecated HTTP API, and offers a more secure and robust mechanism in the same format.

Voilà maintenant ce qui explique que l’API HTTP ne fonctionne pas complètement sur Leia.

Maintenant que l’on sait quoi faire, débutons l’application. La première étape est de créer un nouveau projet FireMonkey. Dans la Form il faut insérer un TIdHTTP et un contrôle TButton.

Étant donnée que JSON sera utilisé, il faut ajouter cette ligne à votre fichier d’en-tête.

#include <System.JSON.hpp>

J’ai décidé de faire une petite classe minimaliste pour supporter JSON-RPC. Voici un autre bout de code à ajouter dans le fichier d’en-tête.

class TJsonRpc
{
public:
    __fastcall TJsonRpc() : Version("2.0") {}
    inline virtual __fastcall ~TJsonRpc() {}

    String Version;
    String Method;
    String Id;

    String __fastcall ToString()
    {
        TJSONObject* Obj = new TJSONObject();
        Obj->AddPair("jsonrpc", Version);
        Obj->AddPair("method", Method);
        Obj->AddPair("id", Id);
        TJSONObject* Params = new TJSONObject();
        Obj->AddPair("params", Params);
        const String Result = Obj->ToString();
        delete Obj;
        return Result;
    }
};

Finalement, il faut mettre ce code dans l’évènement OnClick du bouton.

    IdHTTP1->Request->ContentType = "application/json";

    TJsonRpc LJsonRpc;
    LJsonRpc.Method = "VideoLibrary.Scan";
    LJsonRpc.Id = "mybash";

    const String LUrl =  "http://libreelec:8080/jsonrpc";

    TStringStream* LData = NULL;
    try
    {
        LData = new TStringStream(LJsonRpc.ToString());

        const String LAnswer = IdHTTP1->Post(LUrl, LData);

        TJSONObject* Obj = static_cast<TJSONObject*>(TJSONObject::ParseJSONValue(LAnswer));
        try
        {
            TJSONPair* Pair;
            if((Pair = Obj->Get("error")) != NULL)
            {
                String LMessage;
                int LCode;
                TJSONObject* ErrorObj = static_cast<TJSONObject*>(Pair->JsonValue);
                if((Pair = ErrorObj->Get("message")) != NULL)
                {
                    TJSONString* Answer = static_cast<TJSONString*>(Pair->JsonValue);
                    LMessage = AnsiDequotedStr(Answer->ToString(), '\"');
                }
                if((Pair = ErrorObj->Get("code")) != NULL)
                {
                    TJSONNumber* Answer = static_cast<TJSONNumber*>(Pair->JsonValue);
                    LCode = Answer->AsInt;
                }
                throw Sysutils::Exception("Erreur " + String(LCode) + ": " + LMessage);
            }
        }
        __finally
        {
            delete Obj;
        }
    }
    __finally
    {
        delete LData;
    }

Même si je gère les erreurs d’API, il ne s’agit pas de code de production. Je ne vérifie pas que le ID de retour correspond à celui envoyé. Au lieu de tout mettre dans un évènement de bouton on aurait mieux fait de mettre dans une méthode séparée. On aurait pu aussi gérer la communication dans un thread séparé pour ne pas bloquer l’interface. Mais bon… l’idée c’était de montrer que la meilleure manière de communiquer avec Kodi est l’utilisation de JSON-RPC par HTTP.

J’espère que cet article vous sera utile.

Problème de client et/ou serveur web?

Durant le développement d’une application sous C++Builder j’ai frappé un mur assez solide. Pendant presque qu’un mois il y a eu un problème que je n’ai su résoudre. Maintenant j’ai la solution et je vais vous la partager.

L’application en question est un client HTTP qui utilise les composants TIdHTTP et TIdSSLIOHandlerSocketOpenSSL. Elle possède la possibilité de se connecter à plusieurs serveur web dont certains qui utilisent une connexion sécurisée. Les serveurs fournissent un service à l’aide d’un API JSON.

La plupart de mes tests se faisaient avec un serveur Windows Server 2012 R2 qui utilise un certificat SSL généré gratuitement par Let’s Encrypt. Le certificat est renouvelé automatiquement par l’intermédiaire de l’application Windows ACME Simple, anciennement connu sous le nom letsencrypt-win-simple.

Le développement de l’application allait bon train, jusqu’au moment où j’ai commencé à recevoir l’exception suivante lors de l’utilisation de la méthode Get:

Project app.exe raised exception class EIdSocketError with message ‘Socket Error # 10054 Connection reset by peer.’.

Bien évidemment j’ai pensé que j’avais changé quelque chose dans mon code. Alors j’ai tenté d’utiliser le code de soumissions antérieures. J’utilise toujours Git, même pour mes projets personnels de petite envergure. Dans ce cas, cela aurait pu me sauver la vie, mais non, mon code qui fonctionnait ne fonctionne plus!

Je n’utilisais pas les derniers fichiers DLL de OpenSSL, alors j’ai téléchargé la dernière version. Au moment d’écrire ces lignes la version 1.0.2o était la version LTS la plus à jour. Malheureusement ceci ne provoqua aucun changement de comportement.

Il est important de mentionner que le problème survient seulement avec mon serveur Windows Server 2012 R2 et que l’API répond correctement sur celui-ci avec Chrome et Firefox. Donc, mon serveur fonctionne avec certains clients et mon application fonctionne avec tous les autres serveurs testés! Est-ce un problème de client ou de serveur?

Étant donné que je voulais continuer de développer mon application j’ai décidé d’utiliser un autre serveur pour faire mes tests. La bonne nouvelle, c’est que mon application pouvait progresser, la mauvaise c’est qu’il est possible que certains utilisateurs éprouvent le même problème. Pendant presque un mois ce problème est resté dans ma tête.

L’application tirant à sa fin, je n’avais plus qu’un seul problème à régler. Aux grand maux, les grands moyens, je décide d’utiliser Wireshark pour regarder ce qui se trame lors de la connexion. La seule chose que j’observe est qu’après une requête HTTPS (Client Hello) au serveur, celui-ci semble se déconnecter.

Je me dis alors que c’est peut-être une mise à jour manquante sur le serveur! Alors je fais toutes mes mises à jour Windows, même celles facultatives. J’en profite même pour désactivé TLS version 1.0. Je redémarre le serveur et malheureusement, ceci ne règle rien. C’était quand même une bonne idée de fixer les trous de sécurités sur le serveur.

Par la suite, je vais dans la console de IIS et pour la première fois j’y vois une alerte qui attire mon attention:

No default SSL site has been created. To support browsers without SNI capabilities, it is recommended to create a default SSL site.

Je commence donc à lire un peu sur le sujet et je me rappelle que dans l’outil SSL Server Test de SSL Labs il y avait une bannière bleue avec le texte suivant:

This site works only in browsers with SNI support.

Ma lecture m’amène à comprendre que Server Name Indication (SNI) fait partie du Client Hello. Tiens, cela me rappelle ce que j’ai vu dans Wireshark. C’est le client qui doit supporter cette extension. Dans mon cas, le client est la bibliothèque Indy. Est-ce qu’il y a des problèmes connus avec SNI dans Indy? Le support SNI a été introduit dans la soumission 5321 du 11 janvier 2016. J’utilise RAD Studio XE8 qui date de 2015. Pour être certain, je regarde aussi le fichier Idglobal.hpp où l’on trouve l’information sur la version de Indy utilisé:

#define gsIdVersionMajor 10
#define gsIdVersionMinor 6
#define gsIdVersionRelease 2
#define gsIdVersionBuild 5263

Eh bien, la version de Indy que j’utilise ne supporte pas SNI!

Pour faire un test rapide, je désactive SNI sur mon serveur:

Require Server Name Indication
La case à cocher Require Server Name Indication

Tout fonctionne!

En conclusion, je présume que ce qui s’est passé durant le développement de l’application est que le serveur sur Windows Server 2012 R2 a dû renouveler le certificat SSL et que la configuration du serveur à changer. D’ailleurs lorsque l’on regarde l’historique de soumission du logiciel Windows ACME Simple, plusieurs messages sont en lien avec SNI. J’ai fait une mise à jour à la version 1.9.10.1 et j’espère que lors du prochain renouvellement il n’y aura pas de problèmes. S’il y en a, au moins je sais où regarder en premier.

Utiliser l’API Graph de Facebook avec C++Builder

Facebook se passe sans doute de présentation, par contre son API est peut-être moins connu. Dans cet article nous irons chercher les informations publiques d’un utilisateur qui ne nécessitent aucune autorisation.

La première étape est de créer un nouveau projet FireMonkey HD. Dans la Form il faut insérer un TIdHTTP, un TIdSSLIOHandlerSocketOpenSSL, un TStringGrid, un TImage, un contrôle TEdit, un TLabel et finalement un TButton. Vous pouvez donner comme texte à votre bouton le mot « Rechercher » et pour le TLabel vous pouvez y inscrire « Nom d’utilisateur: ». Je vous propose de placer les composants dans la fenêtre de la manière suivante:
Pour ceux qui se le demande j’ai utilisé le style Air.Style. Ça change un peu des fenêtres Windows que l’on voit tout le temps.

Dans votre fichier cpp voici le fichier d’en-tête à ajouter:

#include <Data.DBXJSON.hpp>

Voici le code à ajouter dans votre constructeur:

    IdHTTP1->IOHandler = IdSSLIOHandlerSocketOpenSSL1;

    // Ceci est nécessaire pour les redirections
    IdHTTP1->HandleRedirects = true;

    // Propriété par défaut pour le contrôle grille
    StringGrid1->ShowSelectedCell = false;
    StringGrid1->ReadOnly = true;
    StringGrid1->RowCount = 0;

    // Ajout de la première colonne
    StringGrid1->AddObject(new TStringColumn(this));
    StringGrid1->Columns[0]->Header = L"Nom";
    StringGrid1->Columns[0]->Width = 150;

    // Ajout de la deuxième colonne
    StringGrid1->AddObject(new TStringColumn(this));
    StringGrid1->Columns[1]->Header = L"Valeur";
    StringGrid1->Columns[1]->Width = 150;

Étant donné que nous accéderons à un site web qui utilise SSL (https), la première ligne de code est critique. Sans elle, une exception dans la classe EIdIOHandlerPropInvalid produira le message « IOHandler value is not valid ». Parce que nous utilisons OpenSSL, les fichiers ssleay32.dll et libeay32.dll devront être distribués avec votre application.

La prochaine étape est d’ajouter le code dans l’événement OnClick du bouton.

    System::Classes::TMemoryStream* ResponseContent = new System::Classes::TMemoryStream;

    try
    {
        // On vide la liste avant d'ajouter les valeurs
        StringGrid1->RowCount = 0;
        Image1->Bitmap = NULL;

        String URL = "https://graph.facebook.com/" + Edit1->Text;

        String Response = IdHTTP1->Get(URL);

        TJSONObject* Obj = static_cast<TJSONObject*>(TJSONObject::ParseJSONValue(Response));
        TJSONPair* Pair;
        TJSONString* Answer;

        if((Pair = Obj->Get("id")) != NULL)
        {   // ID Facebook
            const int Pos = StringGrid1->RowCount;
            StringGrid1->RowCount++;
            Answer = static_cast<TJSONString*>(Pair->JsonValue);
            StringGrid1->Cells[0][Pos] = "ID";
            StringGrid1->Cells[1][Pos] = AnsiDequotedStr(Answer->ToString(), '\"');
        }
        if((Pair = Obj->Get("name")) != NULL)
        {   // Nom complet
            const int Pos = StringGrid1->RowCount;
            StringGrid1->RowCount++;
            Answer = static_cast<TJSONString*>(Pair->JsonValue);
            StringGrid1->Cells[0][Pos] = "Nom";
            StringGrid1->Cells[1][Pos] = AnsiDequotedStr(Answer->ToString(), '\"');
        }
        if((Pair = Obj->Get("first_name")) != NULL)
        {   // Prénom
            const int Pos = StringGrid1->RowCount;
            StringGrid1->RowCount++;
            Answer = static_cast<TJSONString*>(Pair->JsonValue);
            StringGrid1->Cells[0][Pos] = "Prénom";
            StringGrid1->Cells[1][Pos] = AnsiDequotedStr(Answer->ToString(), '\"');
        }
        if((Pair = Obj->Get("last_name")) != NULL)
        {   // Nom de famille
            const int Pos = StringGrid1->RowCount;
            StringGrid1->RowCount++;
            Answer = static_cast<TJSONString*>(Pair->JsonValue);
            StringGrid1->Cells[0][Pos] = "Nom de famille";
            StringGrid1->Cells[1][Pos] = AnsiDequotedStr(Answer->ToString(), '\"');
        }
        if((Pair = Obj->Get("gender")) != NULL)
        {   // Sexe (female ou male)
            const int Pos = StringGrid1->RowCount;
            StringGrid1->RowCount++;
            Answer = static_cast<TJSONString*>(Pair->JsonValue);
            StringGrid1->Cells[0][Pos] = "Sexe";
            StringGrid1->Cells[1][Pos] = AnsiDequotedStr(Answer->ToString(), '\"');
        }

        // Téléchargement de l'image
        IdHTTP1->Get(URL + "/picture", ResponseContent);

        Image1->Bitmap = new Fmx::Types::TBitmap(ResponseContent);
    }
    catch(...)
    {
    }

    delete ResponseContent;

Dans le code on insère dans la liste seulement quelques informations, mais il en existe plusieurs autres qui sont disponibles.

À présent, vous connaissez le minimum requis pour commencer à vous amuser avec cette interface API .

Utiliser l’API Google Street View Image

L’API Google Street View Image permet de télécharger une image statique par l’envoi d’une requête HTTP standard.

La première étape est de créer un nouveau projet FireMonkey HD. Dans la Form il faut insérer un TIdHTTP, un TImage, un TEdit, un TTrackBar et quatre contrôles TButton. Je vous propose de placer les composants dans la fenêtre de la manière suivante:

Google Street View Image API

Le TEdit servira à entrer les coordonnées de latitude et longitude. La barre graduée permet d’agrandir et de réduire l’affichage de l’image. Un nombre plus petit signifie un plus grand niveau de zoom. Les boutons servent à déplacer la caméra dans différentes directions. Le résultat sera évidemment affiché dans le composant TImage.

Voici les attributs et la méthode à ajouter à votre fichier .h:

int FHeading;
int FFieldOfView;
int FPitch;

void __fastcall UpdateImage();

Voici tout le code qui sera nécessaire pour l’application:

//---------------------------------------------------------------------------
#include <fmx.h>
#pragma hdrstop
#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.fmx"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
    // On fait croire à Google que l'on est Firefox 16.0
    IdHTTP1->Request->UserAgent =
        "Mozilla/5.0 (Windows NT 5.1; rv:16.0) Gecko/20100101 Firefox/16.0";

    // Valeur par défaut
    FHeading = 0;
    FFieldOfView = 90;
    FPitch = 0;

    TrackBar1->Tracking = false;
    TrackBar1->Value = FFieldOfView;
    TrackBar1->Min = 10;
    TrackBar1->Max = 120;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::UpdateImage()
{
    System::Classes::TMemoryStream* ResponseContent = new System::Classes::TMemoryStream;

    try
    {
        String URL = Format("http://maps.googleapis.com/maps/api/streetview?size=%dx%d&location=%s&sensor=false&heading=%d&fov=%d&pitch=%d",
            ARRAYOFCONST((
            (int)Image1->Width, // Image Width
            (int)Image1->Height, // Image Height
            EditLocation->Text, // Location
            FHeading, // Heading (0 to 360)
            FFieldOfView, // Field of view
            FPitch // Pitch
            )));

        IdHTTP1->Get(URL, ResponseContent); // Téléchargement de l'image

        Image1->Bitmap = new Fmx::Types::TBitmap(ResponseContent);
    }
    catch(...)
    {
    }

    delete ResponseContent;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::EditLocationKeyUp(TObject *Sender, WORD &Key,
          System::WideChar &KeyChar, TShiftState Shift)
{
    if(Key == vkReturn)
    {   // La touche Entrée a été appuyée, on met à jour l'image
        UpdateImage();
    }
}
//---------------------------------------------------------------------------
void __fastcall TForm1::TrackBar1Change(TObject *Sender)
{
    FFieldOfView = TrackBar1->Value;
    UpdateImage();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::ButtonLeftClick(TObject *Sender)
{
    // Déplacement de la caméra d'un angle de 20° vers la gauche
    FHeading = (FHeading - 20) % 360;
    UpdateImage();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::ButtonRightClick(TObject *Sender)
{
    // Déplacement de la caméra d'un angle de 20° vers la droite
    FHeading = (FHeading + 20) % 360;
    UpdateImage();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::ButtonTopClick(TObject *Sender)
{
    if((FPitch += 10) > 90)
    {   // La caméra est complètement vers le haut
        FPitch = 90;
    }
    UpdateImage();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::ButtonBottomClick(TObject *Sender)
{
    if((FPitch -= 10) < -90)
    {   // La caméra est complètement vers le bas
        FPitch = -90;
    }
    UpdateImage();
}
//---------------------------------------------------------------------------

Le code qui est le plus important se trouve dans la méthode UpdateImage. Tous les évènements servent uniquement à modifier l’un des paramètres du URL.

J’aurais bien aimé utiliser le composant TLocationSensor disponible dans C++Builder XE3 pour aller chercher mes coordonnées de latitude et longitude, mais je n’ai pas le matériel nécessaire pour le tester. C’est dommage car ça me semble facile à utiliser.