En continuant à naviguer sur mon site, vous consentez à ce que j'utilise des cookies pour collecter les statistiques de visites. En savoir plus

#Informatique Développement du site Utopic Radio - Partie 1 : Récupération des tags

Première partie de tout un dossier expliquant la mise en place du site Internet d'Utopic Radio. En espérant que cette série d'articles permettra à toutes les radios d'avoir de nouvelles techniques pour avoir un site Internet attractif et innovant.

La première partie de tout ce dossier concerne les tags qui sont très souvent gérés de manière approximative par les serveurs de diffusions ouverts au grand public.

La norme ICY qui permet d'envoyer les tags via le flux audio d'un serveur fonctionne d'une manière un peu particulière : lorsque le flux est connecté, il envoie une information pour indiquer qu'il utilise le protocole ICY, et indique au client audio à quelle fréquence il doit analyser le paquet de données qu'il reçoit pour en extraire les tags et non des données audio. C'est un peu du bricolage mais c'est comme ça.

C'est le cas du flux de Radionomy, et des flux générés par Shoutcast et Icecast en règle générale. Mais fort heureusement, on dispose généralement d'autres moyens pour récupérer ces données, même s'ils ne sont pas tous idéaux. Dans cet article, on va détailler le cas de Radionomy, mais avant je vous livre une petite piste pour extraire les tags de votre flux Shoutcast : essayez d'accéder à l'URL http://monfluxshoutcast.tld:8000/7

Par chance, donc, Radionomy propose une API. Simpliste, mais suffisante pour récupérer les tags que l'on désire. Commencez par appeler l'URL suivante dans votre navigateur :

http://api.radionomy.com/currentsong.cfm?radiouid=XXXX&apikey=YYYY&callmeback=yes&type=xml&cover=yes

Attention à bien remplacer le XXXX par votre UID de radio, et le YYYY par votre clef d'API. Vous trouverez ces informations dans les paramètres (settings) du RMO de Radionomy.

Voici un exemple de flux généré par l'API :

<tracks>
    <radioname>Utopic Radio</radioname>
    <rank>0</rank>
    <isradionomy>1</isradionomy>
    <radurl>http://www.radionomy.com/utopicradio</radurl>
    <track>
        <uniqueid>2516030630</uniqueid>
        <title>I'm Outta Love</title>
        <artists>Anastacia</artists>
        <starttime>2015-08-19 21:01:34.25</starttime>
        <playduration>198500</playduration>
        <current>1</current>
        <cover>
            http://i.radionomy.com/document/tracks/c/c91a/c91a088c-ad96-4c17-bbc5-73f78d925623.jpg
        </cover>
        <callmeback>169295</callmeback>
    </track>
</tracks>

Ici, plusieurs informations vont nous intéresser : le titre de la musique (title), l'artiste (artists), la photo associée qui se trouve souvent être la couverture d'album (cover), et la durée en millisecondes pendant laquelle nous allons devoir patienter avant de rappeler l'API (callmeback).

Attention : votre site Internet qui va exécuter des requêtes pour obtenir les tags ne va pas interroger Radionomy directement. D'abord parce que vous risquez d'avoir quelques surprises au niveau des tags que vous ne voudriez pas afficher sur votre site ; ensuite, parce que si vous faites ça, n'importe qui va avoir accès à votre radiouid et votre apikey, ce qui n'est pas souhaitable.

Nous allons donc faire appel à un script PHP intermédiaire qui va masquer tout ce que vous ne voulez pas montrer aux yeux du public.

Pour commencer, on va admettre que vous savez parser des données XML telles que celles-ci, mais comme je suis gentil, je vous propose un bout de code en PHP :

$data = file_get_contents('http://api.radionomy.com/currentsong.cfm?radiouid=XXXX&apikey=YYYY&callmeback=yes&type=xml&cover=yes');
$doc = DOMDocument::loadXML($data);
$title = $doc->getElementsByTagName('title')->item(0)->nodeValue;
$artist = $doc->getElementsByTagName('artists')->item(0)->nodeValue;
$timeout = $doc->getElementsByTagName('callmeback')->item(0)->nodeValue;
$cover = $doc->getElementsByTagName('cover')->item(0)->nodeValue;

Nous avons toutes les données pour commencer à travailler avec.

À ce stade, comprenez bien que, quelque soit la manière dont on récupère les informations, il faut avoir extrait les informations que l'on ne peut pas deviner nous-même : le titre et l'artiste.

Mais les données reçues ne me satisfont pas pleinement, pour deux raisons. La première étant que la couverture envoyée par Radionomy est parfois une couverture de compilation faite par d'autres radios (!), un peu limite d'afficher en gros un album NRJ sur le site d'une autre webradio. Mais on en voudra pas à Radionomy qui n'a pas forcément choisi le bon prestataire de données pour recevoir ces couvertures (anecdote, il semblerait d'ailleurs que ça soit le même prestataire que Windows quand il vous télécharge des couvertures d'albums pour vos fichiers MP3 téléchargés de manière tout à fait légale).

Comme la couverture ne me plaît pas, disais-je, je vais essayer d'interroger Spotify. J'ai choisi Spotify, d'une part parce qu'ils possèdent une grande base de données musicale, et aussi parce que l'API est en accès libre. Évidemment Spotify n'a pas réponse à tout, et on devra donc se contenter de la couverture de Radionomy si aucun résultat n'est retourné, voire d'absolument aucune couverture. Je vous explique tout :

function trySpotify($artist, $title, $cover) {
        $q = explode(' - ', $artist)[0] . ' ' . $title;
        $request = 'https://api.spotify.com/v1/search?q='.urlencode($q).'&type=artist,track';
        $result = json_decode(file_get_contents($request));
        if ($result->tracks->total == 0) {
                return $cover;
        }
        $i = 0;
        while ($result->tracks->items[$i]->album->album_type != 'album' && $i <= $result->tracks->total) {
                $i++;
        }
        if ($i == $result->tracks->total) {
                return $cover;
        }
        return $result->tracks->items[$i]->album->images[0]->url;
}

La fonction demande 3 paramètres : l'artiste, le titre et la couverture à appliquer par défaut.

Ici, la première opération consiste à extraire l'artiste principal de la musique. Le flux XML envoyé par Radionomy n'est pas formaté de manière idéale. Et là on en arrive au deuxième truc gênant : en effet, plutôt que d'envoyer une liste d'artistes qui pourrait être formatée sous cette forme :

<artists>
    <artist>Alan Parsons</artist>
    <artist>Chris Rainbow</artist>
</artists>

On reçoit ceci :

<artists>Alan Parsons - Chris Rainbow</artists>

Par chance, il est très rare qu'un artiste possède dans son nom de scène les caractères espace-trait d'union-espace à la suite. On va donc se baser sur cette chaîne de caractères pour séparer les artistes lorsqu'il y en a plusieurs. Par chance, une fois de plus, la coutume est d'indiquer l'artiste principal en premier dans la liste. Et c'est cet artiste qui va nous intéresser pour l'interrogation de l'API de Spotify.

L'astuce est la suivante :

explode(' - ', $artist)[0] // => Expression (A)

explode() est une fonction qui va renvoyer un tableau d'éléments séparés par la chaîne de caractères envoyée en premier paramètre de la fonction. Exemple :

explode(',', 'abricot,banane,citron');

produira :

['abricot', 'banane', 'citron']

Le gros avantage d'explode() étant qu'elle renvoie un tableau d'un seul élément (la chaîne entière) s'il n'y a rien à séparer. Concrètement cela veut dire que pour une chanson avec un seul artiste, l'expression A renverra l'artiste en question, sinon, le premier artiste de la liste sera retourné.

Ensuite, on appelle l'API de Spotify à l'aide de cette URL :

https://api.spotify.com/v1/search?q=terms+of+search&type=artist,track

Cette URL indique que nous allons chercher l'expression "terms of search" dans la base de données de Spotify, en espérant obtenir des artistes ou des titres. Ici, on va bien sûr remplacer "terms of search" par ce qui nous intéresse, à savoir :

$q = explode(' - ', $artist)[0] . ' ' . $title;
# produira une chaîne ressemblant à "Alan Parons Eye In The Sky".
$request = 'https://api.spotify.com/v1/search?q='.urlencode($q).'&type=artist,track';
# produira une chaîne ressemblant à https://api.spotify.com/v1/search?q=Alan+Parsons+Eye+In+The+Sky&type=artist,track

Je vous conseille d'ouvrir cette URL dans votre navigateur pour voir ce à quoi vous allez avoir affaire. Oui, c'est un bon gros flux JSON imbitable. Heureusement, je l'ai décodé pour vous, et je vous fourni déjà tout l'algorithme pour récupérer la couverture Spotify. Mais quelques explications s'imposent.

Tout d'abord, il faut s'assurer qu'on a bien des résultats qui s'offrent à nous, sinon on renvoie la couverture de Radionomy :

if ($result->tracks->total == 0) {
    return $cover;
}

Ensuite, on parcourt la liste des résultats renvoyés par Spotify pour trouver la première couverture d'album et non de compilation. Généralement, ça marche plutôt bien parce que les résultats sont triés par popularité… seul bémol (et là on ne pourra rien faire de plus, à part peut-être changer d'API), certaines compilations d'artistes ou compilations faites par des labels et des maisons de disques (genre "Méga Top Années Disco 70 en 200 volumes de 20 tubes chacun") sont encore considérées par Spotify comme des albums… Mais ça tend à évoluer petit à petit, et vous vous rendrez vite compte que les couvertures, pour une même musique, ne seront pas toujours les mêmes (un peu de diversité, c'est pas si mal).

$i = 0;
while ($result->tracks->items[$i]->album->album_type != 'album' && $i <= $result->tracks->total) {
    $i++;
} // oui, là on aurait pu aussi faire un for()
if ($i == $result->tracks->total) {
    return $cover; // Si on a fait tout le tour et qu'on a trouvé aucun album, on renvoie la couverture de Radionomy qui sera certainement plus appropriée.
}
return $result->tracks->items[$i]->album->images[0]->url; // on renvoie la première image parce que c'est la plus grande, et on veut des images avec lesquelles on peut facilement travailler.

Voilà pour la fonction trySpotify(). Si après cela vous n'avez toujours pas de couverture, notamment parce que Radionomy ne vous en a pas fourni non plus (ça arrive), pensez à afficher un fallback : soit une couverture bidon (genre un pictogramme représentant un CD dans son boîtier), soit vous pouvez utiliser le logo de votre radio (parce qu'on ne le verra jamais assez, il faut que les gens connaissent votre image de marque).

À ce stade, on a la couverture et les tags. Cependant, les diffuseurs font rarement la différence entre une musique, un jingle et un spot publicitaire (ça arrive, mais pas dans notre cas ici).

On va donc apprendre à différencier tout ça. Tout d'abord, pour tous vos éléments d'habillage sonore, prenez l'habitude d'indiquer le nom de votre radio en tant qu'artiste de votre fichier audio (et toujours inscrit de la même manière). Pour ma part, tous mes jingles et autres woosh et liners ont comme artiste "Utopic".

Pourquoi le nom de votre radio ? Parce que l'artiste des jingles, c'est vous (ou presque, ça dépend si vous avez fait appel à quelqu'un d'autre, mais le fait est que ce son appartient à votre radio). En titre, vous pouvez mettre ce que vous voulez.

Ainsi, quand vous allez détecter que l'artiste reçu correspond au nom de votre radio, arrangez-vous pour retourner autre chose. Pour ma part, j'affiche le nom de ma radio comme artiste, le slogan en titre, et le logo en image de couverture.

La même gymnastique doit opérer si vous rencontrez des écrans de publicité. Chez Radionomy, les tags que vous recevrez dans cette situation sont les mêmes pour le titre et l'artiste : 'ADVERT:TARGETSPOT'.

Comme je ne fais pas de live, je ne sais pas ce qui est envoyé par Radionomy pendant les directs, mais si, dans cette situation, vous avez le choix de vos tags (ce qui est le cas de certains diffuseurs), soyez inventifs ! Par exemple, envoyez 'EMISSION' en artiste et un code d'émission arbitraire en titre. Ainsi dans notre script, si, en lisant les tags du flux, on rencontre ces données, le code d'émission sera notre référence pour faire appel à une autre base de données (celle de votre site Internet par exemple) pour renvoyer les données adéquats à vos visiteurs : par exemple, en artiste, le nom de l'émission, en titre une (très) courte description (le slogan de l'émission par exemple), et en image de couverture, une photo des animateurs, ou le logo.

Le site doit ensuite se charger d'afficher les données reçues (en JSON évidemment) et avec jQuery, ça peut se faire relativement facilement :

(function($) {

    var getTags = function() {
        $.getJSON('/tags.php', function(data) {
            $('.artist').text(data.artist);
            $('.title').text(data.title);
            $('.cover').attr('src', data.cover);
            setTimeout(getTags, data.callmeback);
        });
    };
    getTags();

})(jQuery);

Résumons tout cela

  1. On récupère les tags de Radionomy
  2. On essaie d'obtenir une superbe couverture d'album
  3. On effectue des traitements de faveur en fonction des tags reçus
    • Si on a une chanson, on renvoie le tout sans ménagement à notre site Internet
    • Si on a affaire à un élément d'habillage ou une pub, on affiche le logo de la radio, le nom et le slogan
    • Si on est en présence des tags d'une émission, on renvoie le nom de l'émission, son slogan et son logo ou une photo des animateurs.
  4. On renvoie bien sûr toutes ses informations au site Internet qui fera le traitement en javascript.

Attention : tout ce mécanisme ne sera fonctionnel que là où vous utiliserez vous-même ces comportements. C'est-à-dire que les supers tags et les belles photos s'afficheront bien sur votre site, ou sur votre application mobile. Mais si vous proposez l'URL de votre flux à quiconque voudra vous écouter sur un autre lecteur (VLC, iTunes, Windows Media Player, ou que sais-je), ces auditeurs ne bénéficieront pas de tous ces fabuleux avantages, mais ce sont eux qui l'auront voulu…


Pour terminer, je vous passe les codes sources bruts (qui accessoirement pourraient certainement être optimisés).

tags.php

session_start();
    function trySpotify($artist, $title, $cover) {
            $q = explode(' - ', $artist)[0] . ' ' . $title;
            $request = 'https://api.spotify.com/v1/search?q='.urlencode($q).'&type=artist,track';
            $result = json_decode(file_get_contents($request));
            if ($result->tracks->total == 0) {
                    return $cover;
            }
            $i = 0;
            while ($result->tracks->items[$i]->album->album_type != 'album' && $i <= $result->tracks->total) {
                    $i++;
            }
            if ($i == $result->tracks->total) {
                    return $cover;
            }
            return $result->tracks->items[$i]->album->images[0]->url;
    }

    $radioName = 'Ma Super Radio';
    $radioSlogan = 'Mon slogan 100% génialissime !';
    $radioLogo = __DIR__.'/chemin/physique/vers/le/logo/de/votre/radio.png';
    $advertTagsArtist = 'ADVERT:TARGETSPOT';
    $coverPath = '/cover.png';
    $radioTagsArtist = strtoupper($radioName);

    $data = file_get_contents("http://api.radionomy.com/currentsong.cfm?radiouid=XXXX3&apikey=YYYY&callmeback=yes&type=xml&cover=yes");

    $isSong = true;
    $doc = DOMDocument::loadXML($data);
    $title = $doc->getElementsByTagName('title')->item(0)->nodeValue;
    $artist = $doc->getElementsByTagName('artists')->item(0)->nodeValue;
    $timeout = $doc->getElementsByTagName('callmeback')->item(0)->nodeValue;
    $cover = $doc->getElementsByTagName('cover')->item(0)->nodeValue;

    $cover = trySpotify($artist, $title, $cover);

    if ($title != $_SESSION['tags'] && $artist != $_SESSION['tags']) {
            $ref = strtoupper($artist);

            if ($ref == $advertTagsArtist || $ref == $radioTagsArtist) {
                    $artist = $radioName;
                    $title = $radioSlogan;
                    $cover = $radioLogo;
                    $isSong = false;
            }

            $contents = file_get_contents($cover);
            if (empty($contents)) {
                    $contents = file_get_contents($radioLogo);
            }
            file_put_contents(__DIR__.$coverPath, $contents);

            $_SESSION['tags'] = [
                    'title' => $title,
                    'artist' => $artist,
                    'cover' => $coverPath . '?v=' . time(),
                    'timeout' => $timeout
            ]; // On garde les tags en session pour ne pas avoir à tout recalculer à chaque fois
    }

    echo json_encode($_SESSION['tags']);

Et côté client :

var timeoutGetTags = null;
var previousTags = null;
var tagsUrl = '/tags.php';
var tagsUpdateEvt = 'tags-update';

window.radioName = 'Utopic';
window.radioSlogan = 'Ta webradio 200% smile !';
window.radioLogo = window.location.origin + '/theme/utopic/logo_icon_utopic.png';
window.fullRadioName = radioName + ' - ' + radioSlogan;

(function($){$(window).load(function(){

    // TAGS

    var getTags = function() {
            $.getJSON(tagsUrl, function(data) {
                    if (previousTags != data.title) {
                            previousTags = data.title; // on se base sur le titre qui a plus de chance de changer surtout si vous faites une session spéciale pour un seul artiste
                            $(window).trigger(tagsUpdateEvt, data);
                    }
                    if (timeoutGetTags) clearTimeout(timeoutGetTags);
                    timeoutGetTags = setTimeout(getTags, data.timeout);
            });
    };
    setInterval(getTags, 15000); // Même si on dispose d'un callmeback, je sécurise la mise à jour systématique des tags en faisant un appel toutes les 15 secondes.
    getTags();

    // TAGS UPDATE EVENT MGMT

    $(window).on(tagsUpdateEvt, function(e, data) {
            var artist = data.artist;
            var title = data.title;
            var cover = data.cover;

            $('.player .cover').css('background-image', 'url(' + cover + ')');
            $('.player .artist').text(artist);
            $('.player .title').text(title);
    });
});})(jQuery);

À vous d'adapter ces sources selon vos besoins !

Dernier petit avertissement, faites bien attetion à avoir un serveur qui supportera la charge de plusieurs interrogations de tags de manière très très régulière…

Rédigé le .

Commentaires

comments powered by Disqus