Jouer avec JavaFX, JSON, HTTP et Jiwa

Bijour,

Au commencement de mon blog Dieu créa la terre, le ciel, linux, javaFX venait de sortir et j’avais publié quelques liens intéressant sur la bestiole, mais j’avais pas fait grand chose avec.

Aujourd’hui … j’ai essayé de faire un player musical version Desktop du genre Spotify en JavaFX (parce que Deezer ou Jiwa font pas mal ramer les machines utilisant flash sous linux), une appli qui devait se fournir sympatiquement en musique chez Jiwa.

Plantage du décors et quelques avancement du soft :

La recherche de titres, d’albums ou artistes se fait via une requête en post à l’url http://www.jiwa.fr/track/search, et la réponse est de type :

{"page":[
{"trackId":133337,
"songId":1337,
"songName":"les kikoolols attaquent!!",
"artistId":51337,
"artistName":"KikoololMaster",
"secArtistsNames":null,
"albumId":7331,
"albumName":"Les Kikoolols vaincront",
"songPopularity":69,
"itunesTrackUrl":null,
"albumReleaseDate":"1969-01-01",
"duration":"404",
"hidden":"0",
"sourceId":null}],
"total":1,
"min":0,
"max":1,
"pageSize":25,
"success":true}

Il faut donc faire la requête http POST, avec le paramètre Q = notre recherche :

var connexion : HttpRequest = HttpRequest {
        method: HttpRequest.POST
        location: locationSearch
        onOutput: function(os: java.io.OutputStream) {
            var urlConverter = URLConverter{};
            var pair = Pair {name: "q",value: "Les kikoo roxe !!"};
            var encodedMessage = urlConverter.encodeParameters([pair]);
            os.write(encodedMessage.getBytes());
            os.close();
        }
        onInput: function(is: java.io.InputStream) {
            //récupération du JSON
        }
    };connexion.start();

Rien de complexe jusque-là … on récupère donc notre JSON, grâce à l’objet PullParser :

try {
                json.input = is;
                var tmpElmt = "";
                var i=0;
                json.onEvent = function(event: Event) : Void{
                    if( event.type == PullParser.TEXT ){

                        if(event.name == "artistName"){tmpElmt.concat(event.text);}
                        else if (event.name == "artistName"){tmpElmt=tmpElmt.concat("Artiste : {event.text.trim()} ");i++;}
                        else if (event.name == "albumName"){tmpElmt=tmpElmt.concat("Album : {event.text.trim()} ");i++;}
                        else if (event.name == "songName"){tmpElmt=tmpElmt.concat("Titre : {event.text.trim()} ");i++;}
                        if(i==2){insert tmpElmt.replaceAll("\n", "").trim() into item;i=0;tmpElmt="";}
                    }
                };
                json.parse();
            } finally {is.close();}

On récupère donc le nom de l’album, du chanteur et le titre de la chanson (les autres paramètres sont pour plus tard) et on l’insert dans un tableau nommé « mus » ….

Le problème pour la suite, c’est la récupération de la chanson, qui se passe en deux étapes : la récupération des informations liées au songId et la récupération du mp3.

Grâce au songId l’on vient de choper dans le JSON, on a accès à l’étape suivante via une requête POST à l’url http://m.jiwa.fm/token.php où l’on envoit le paramètre s avec l’id « songId » retourné dans le JSON plus haut, ce qui donne comme réponse un string de la forme : « unMD5=8 chiffres=un zero=7 chiffres=3 chiffres », ce qui permet apparement de crée l’url de téléchargement de la chanson :

http://m.jiwa.fm/play.php?r=8 chiffres&s=le songId&t=un md5&m=7 chiffres&from=0, qui renvoit direct le mp3 ou -1 si les paramètres ne sont pas bon :s

Le gros problème étant que le md5 retourné par token.php, n’est pas celui à fournir dans l’url du play.php.

Il faut donc ouvrir le player swf ( ou se baser sur iJaw pour les flemmards comme moi ) pour choper la « clé », et récupérer le flux.

Le code complet en javaFX (pour le moment ^^, permet de récupérer le flux, et de le lire … en executant le jar!!! Via une compilation direct depuis netbeans, il ne trouve pas le mp3) :

/*
 * Main.fx
 *
 * Created on 3 oct. 2009, 13:07:02
 */

package jiwafx;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.io.http.HttpRequest;
import javafx.io.http.URLConverter;
import javafx.data.Pair;
import javafx.scene.control.ListView;
import javafx.ext.swing.SwingListItem;
import javafx.data.pull.PullParser;
import javafx.data.pull.Event;
import javafx.scene.control.Button;
import javafx.scene.control.TextBox;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.io.http.HttpHeader;
import javafx.scene.input.MouseEvent;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.Media;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.FileOutputStream;
import java.io.BufferedInputStream;
import java.io.InputStream;

/**
 * @author cipher16
 */
//Global var
var item : SwingListItem[] = [];
var mus = [];
//UI items
var scene = Scene {width: 250,height: 250};
var searchTxt = TextBox{width:bind (scene.width - 100),text: "Entrer votre recherche"};
var searchBtn = Button{text: "Rechercher",width: 100,action: function(){searchAlbum(searchTxt.text)};};
var listView : ListView = ListView{width:bind scene.width,height: bind (scene.height - searchTxt.height),items: bind item};
var SearchBox = HBox {content: [searchTxt,searchBtn],width: bind scene.width};
var player = MediaPlayer{autoPlay:true,onBuffering:function(d){println("Encore :  {d}")}};
scene.content = VBox{content:[SearchBox,listView],width: bind scene.width,height: bind scene.height}

function launchDl(url:String) : Void
{
    var ua = HttpHeader{name:"User-Agent",value:"Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.1.3) Gecko/20091020 Ubuntu/9.10 (karmic) Firefox/3.5.3"};
    HttpRequest {
        method: HttpRequest.GET
        headers: [ua]
        location: url
        onInput: function(is: java.io.InputStream) {
            var file = new FileOutputStream('jiwa.mp3');
            while(is.available()>0){
                file.write(is.read());
            }
            is.close();
        }
        onDone:function(){player.media = Media{source:"{__DIR__}jiwa.mp3"};player.play(); }
    }.start();
}

function getToken(id:String)
{
    var ret="";
    var referer = HttpHeader{name:"Referer",value:"http://www.jiwa.fm/res/widget/LightPlayer.swf?=1255325694463"};
    var ua = HttpHeader{name:"User-Agent",value:"Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.1.3) Gecko/20091020 Ubuntu/9.10 (karmic) Firefox/3.5.3"};
    HttpRequest {
        method: HttpRequest.POST
        headers: [referer,ua]
        location: "http://m.jiwa.fm/token.php"
        onOutput: function(os: java.io.OutputStream) {
            var urlConverter = URLConverter{};
            var pairS = Pair {name: "s",value: id};
            var encodedMessage = urlConverter.encodeParameters([pairS]);
            os.write(encodedMessage.getBytes());
            os.close();
        }
        onInput: function(is: java.io.InputStream) {
            var buff = new BufferedReader (new InputStreamReader(is));
            var line = "";
            while((line = buff.readLine())!=null){
                ret="{ret}{line}";
            }
            is.close();
        }
        onDone: function()
        {
            launchDl(getTrackUrl(ret,id));
        }

    }.start();
}

function getTrackUrl(token:String,sid:String)
{
	var t_vals = token.split("=");
        var url="";
        if(t_vals.length>0)
        {
            var a = MD5.toHex('gwqd29ydg7sqys_qsh0');
            var b = MD5.toHex("{t_vals[0]}{a}{sid}");
            url = "http://m.jiwa.fm/play.php?r={t_vals[1]}&s={sid}&t={b}&m={t_vals[3]}&from=0";
        }
        return url;
}

function searchAlbum(search: String)
{
    item = [];//on reset à la barbare 😉
    HttpRequest {
        method: HttpRequest.POST
        location: "http://www.jiwa.fr/track/search"
        onOutput: function(os: java.io.OutputStream) {
            var urlConverter = URLConverter{};
            var pair = Pair {name: "q",value: search};
            var encodedMessage = urlConverter.encodeParameters([pair]);
            os.write(encodedMessage.getBytes());
            os.close();
        }
        onInput: function(is: java.io.InputStream) {
            try {
                var json : PullParser = PullParser {documentType: PullParser.JSON,input:is};
                var tmpElmt = "";
                var i=0;
                var it = SwingListItem{};
                json.onEvent = function(event: Event) : Void{
                    if( event.type == PullParser.TEXT or event.type == PullParser.INTEGER or event.type == PullParser.NUMBER ){
                        if (event.name == "artistName"){tmpElmt=tmpElmt.concat("Artiste : {event.text.trim()} ");i++;}
                        else if (event.name == "albumName"){tmpElmt=tmpElmt.concat("Album : {event.text.trim()} ");i++;}
                        else if (event.name == "songName"){tmpElmt=tmpElmt.concat("Titre : {event.text.trim()}");i++;}
                        else if (event.name == "songId"){i++;it.value="{it.value};{event.integerValue}"}
                        else if (event.name == "artistId"){i++;}//Lol xD Da Kikoolol Attack!!!
                        else if (event.name == "trackId"){i++;it.value="{event.integerValue}"}
                        if(i>=6){ it.text=tmpElmt.replaceAll("\n", "").trim(); insert it into item;i=0;tmpElmt="";it=SwingListItem{};}
                    }
                };
                json.parse();
            } finally {is.close();}
        }
    }.start();
}

listView.onMouseClicked = function(e:MouseEvent){
    player.stop();
    player.media.source = null;
    player.media = null;
    var ids = item[listView.selectedIndex].value.toString().split(";");
    getToken(ids[1]);
}

Stage {title: "JiwaFX",scene: scene}

Et tire cette tête là une fois executé (avec une recherche sérieuse) :

CaptureCe qui est pas trop mal pour un début … j’essaierais de faire la même chose pour deezer, avec la clé de l’API cette fois … histoire de réussir un truc plus abouti^^. J’ai juste posté le code pour aider les nouveaux en JavaFX (pour l’utilisation de requêtes POST en HTTP, très mal documenté par SUN et sur le parsing JSON, encore plus mal documenté …).

Le code est assez crade, car on est obligé d’attendre la fin (probable) de deux évènements (le chargement des deux pages token et play) pour lancer la musique.

L’un des gros problèmes actuel, c’est que Jiwa rejette les User-Agent en Java, ce qui fait que l’on ne peut pas directement filer l’url retourné par la fonction à la source du media, d’où l’obligation de passer par un fichier temporaire.

Voilà, amusez-vous bien

try {
json.input = is;
var tmpElmt = «  »;
var i=0;
json.onEvent = function(event: Event) : Void{
if( event.type == PullParser.TEXT ){

if(event.name == « artistName »){tmpElmt.concat(event.text);}
else if (event.name == « artistName »){tmpElmt=tmpElmt.concat(« Artiste : {event.text.trim()} « );i++;}
else if (event.name == « albumName »){tmpElmt=tmpElmt.concat(« Album : {event.text.trim()} « );i++;}
else if (event.name == « songName »){tmpElmt=tmpElmt.concat(« Titre : {event.text.trim()} « );i++;}
if(i==2){insert tmpElmt.replaceAll(« \n », «  »).trim() into item;i=0;tmpElmt= » »;}
}
};
json.parse();
} finally {is.close();}

7 réflexions sur « Jouer avec JavaFX, JSON, HTTP et Jiwa »

  1. Bonjour,

    Merci pour cet article très instructif qui m’a permit de l’adapter à mes besoins et dans mon langage.

    Cependant j’ai une petite question, comment as-tu fais pour trouver comment était générée la clé à passer en variable au play.php ? Car fallait y penser quand même de concatener la clé retournée par token.php, celle du player swf et le songId, puis mixer tout ça en md5… J’y aurait pas pensé tout seul.. 😀

    En tous cas bravo !

  2. Ah oui effectivement si on décompile le swf… 😀

    Tient en ce moment j’suis en train de regarder ce qui se passe du côté de Deezer, j’ai trouvé quelques trucs pas mal, les requêtes pour faire une recherche etc… Et j’ai trouvé un truc qui m’interpele, si tu envoie une requete de ce type :
    http://api-v3.deezer.com/1.0/gateway.php?method=song_getData&output=3&input=3&api_key=BLWLtJr6dqaxhfgRnxxZBfDjeMEZhk7Uft0aQhip7jDJAsjqHhrgBYN204tjqErv&sid=e75f7403f373e3a65ad07814cc5ee2b5b62ab886 avec en paramétre {« SNG_ID »:906990, »ARRAY_INCLUDE »:[], »CLEARCACHE »:false, »ARRAY_EXCLUDE »:[]}

    (Le SNG_ID étant l’id du titre tu l’aura deviné et le sid étant variable) on obtient le resultat suivant :

    {« error »:[], »results »:{« ALB_ID »: »102424″, »ALB_PICTURE »: »7f3c9bd22049e405c3b30f09cf62a641″, »ALB_TITLE »: »Mechanical Animals (Ecopac explicit) », »ARRANGER »: » », »ART_ID »: »482″, »ART_NAME »: »Marilyn Manson », »AUTHOR »: » », »BPM »: »0″, »COMPOSER »: »Zim Zum », »CREATIVE_COMMON »: » », »DATE_START »: »2008-01-01″, »DIGITAL_RELEASE_DATE »: »2008-08-04″, »DISK_NUMBER »: »1″, »DURATION »: »340″, »EXPLICIT_LYRICS »: »1″, »FILESIZE »: »5413021″, »FILESIZE_MP3_128″: »5413021″, »FILESIZE_MP3_320″: »13532505″, »FULL_PATH_ORIGIN »: »\/data\/music\/import\/lbl_universal\/done_200809\/2000000357684_done\/00600753031889_2000000357684\/UMG_audtrk_00600753031889_01_014_708.mp3″, »GENRE_ID »: »13″, »GRID »: »0000000000000″, »INDEXATION_DATE »: »2008-07-02 14:43:10″, »ISRC »: »USIR19801994″, »KEYWORD »: » », »LABEL_ID »: »17757″, »LANG »: »0″, »MD5_ORIGIN »: »c9e2c2c3701cabe03dd8f31293a93c0c », »NOTE »: »0″, »ORIGIN »: »0″, »PERFORMER »: » », »PHYSICAL_RELEASE_DATE »: »2008-08-04″, »PROVIDER_ID »: »4″, »RANK »: »58″, »SMARTRADIO »: »1″, »SNG_ID »: »906990″, »SNG_ID_NEW »: »0″, »SNG_TITLE »: »Coma White », »SONY_ID »: » », »STATUS »: »1″, »S_ALC »: »0″, »S_MOD »: »1″, »S_PREMIUM »: »0″, »S_STREAMING »: »0″, »S_WIDGET »: »1″, »TRACK_NUMBER »: »14″, »UPDATE_DATE »: »2009-09-18 12:41:12″, »URL_REWRITING »: »marilyn-manson », »USER_ID »:0, »VERSION »: »(Album Version Explicit) »}}

    ce qui m’interpel c’est la section : FULL_PATH_ORIGIN = \/data\/music\/import\/lbl_universal\/done_200809\/2000000357684_done\/00600753031889_2000000357684\/UMG_audtrk_00600753031889_01_014_708.mp3

    Un lien directe vers le mp3 ? Ca pourrait s’tenir, le swf doit bien allé chercher le mp3 à une source, mais je n’arrive pas à trouver la racine du chemin…

    Si t’as une idée… 😀

  3. Intéressant … mais la variable est explicite : PATH et non pas URL, ça doit être par rapport à leurs disques et non par rapport à un serveur accessible en HTTP (d’autant plus que le path fournit ne ressemble en rien aux urls qui fournissent les médias flv via leurs proxies)… à approfondir ^^.

    Mais en regardant la structure des données envoyées, il doit être possible de générer les urls d’accès aux streams assez facilement 😉 par contre va falloir jouer pas mal avec les cookies et les urls de session :s.

    En y regardant de plus prêt, l’url d’un stream se compose de : « /stream/2/51/ff/192 caractères »
    Du 2ème au 4ème slash on peut imaginer que c’est des urls de répartition de charges (car plus ou moins commune sur des recherches successives, et aucune données liées au JSON fournit n’y figure), les 192 caractères restant POURRAIENT être une concaténation de 6 MD5 … à trouver dans les données du JSON renvoyé.

  4. Oé c’est ce que je me suis dis, que ce soit un chemin plutot qu’une url, elle sert peut être pour retrouvé le chemin du titre après avoir passé le proxi ou qu’en sais-je…

    Pour ce qui est de l’URL, comme tu dis le début est assez simple à générer, par exemple, avec le titre que j’ai utilisé precedement, l’URL est de ce type :
    http://proxy-c-v3.deezer.com/stream/2/c9/e2/ ce qui correspond au debut de la variable MD5_ORIGIN retournée par la requete que j’ai cité précedement, et en suite il y a effectivement toute une chaine de MD5, qui peut être, contiennent les informations comme la taille du mp3 (variable également retournée par la requete précedent) étant donné qu’il y a deux tailles possible celon l’encodage du mp3 si on a un compte premium ou non, etc… d’ailleurs j’essaierais de faire le test de hasher en md5 les variables et voir si ca ressemble à ce qu’on trouve dans l’URL.

    Par contre le problème restera le même, les fichiers retournés seront toujours cryptés, et je ne sais pas si la clé de décryptage est fixe ou alors un md5 généré à chaque session… mais ce week end j’ai décompilé le player et j’ai trouvé deux chaines qui sont susceptibles d’être la clé, mais rien est sûre…

  5. Avez vous progressé dans vos recherches ? Je cherche de mon coté, mais malgré la décompilation des fichiers swf, je n’arrive pas à trouver le mode d’élaboration des 192 caractères des url de chargement, et le mode de décryptage des fichiers envoyés.

Les commentaires sont fermés.