Nostalgie

Il me semble que j’étais meilleur avant.

LastFM Scrobbling

il y a quelques temps de celà j’utilisais une classe du projet Ampache pour soumettre des morceaux de musique à LastFM… seulement après un changement de version du protocole de “scrobbling“, cette classe est devenue obsolète. Voilà de quoi la remplacer:

<?php
/**
 * PHP Scrobbler
 *
 * This class lets you submit tracks to a lastfm account. Curl needed.
 *
 * Basic usage:
 *
 * require('md/Scrobbler.php');
 * $scrobbler = new md_Scrobbler('lastfmUser', 'password');
 * $scrobbler->add('Jerry Goldsmith', 'The space jockey', 'alien', 289);
 * $scrobbler->submit();
 *
 * @author Mickael Desfrenes <desfrenes@gmail.com>
 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link www.desfrenes.com
 */
class md_Scrobbler_Exception extends Exception{}
class md_Scrobbler
{
	// change this according to your client id and version
	const clientId      = 'tst';
	const clientVer     = '1.0';

	const SCROBBLER_URL = 'http://post.audioscrobbler.com/?hs=true&p=1.2.1&c=<client-id>&v=<client-ver>&u=<user>&t=<timestamp>&a=<auth>';
	// curl timeout
	const TIMEOUT       = 10;

	// lastfm user
	private $user;
	// lastfm user password
	private $password;
	// scrobbler session id
	private $sessionId         = '';
	// last handshake failure timestamp (0 = no failure)
	private $handShakeFailure  = 0;
	// number of hard failures
	private $submitFailures    = 0;
	// store tracks here
	private $queue             = array();
	private $nowPlayingUrl;
	private $submissionUrl;

	/**
	 * New md_Scrobbler
	 *
	 * @param string LastFM login
	 * @param string LastFM password
	 */
	public function __construct($user, $password)
	{
		$this->user     = $user;
		$this->password = $password;
	}

	/**
	 * Add a track to the queue
	 *
	 * @param string artist name
	 * @param string track title
	 * @param string album title
	 * @param integer track length (seconds)
	 * @param integer track play timestamp
	 * @param integer track number
	 * @param string source type (see lastFM API docs)
	 * @param integer rating
	 * @param string music brain track ID
	 * @return boolean
	 */
	public function add($artist, $track, $album = '', $trackDuration, $scrobbleTime = '', $trackNumber = '', $source = 'P', $rating = '', $mbTrackId)
	{
		if(empty($scrobbleTime))
		{
			$scrobbleTime = time();
		}
		$this->queue[] = array('artist'        => $artist,
							   'track'         => $track,
							   'scrobbleTime'  => $scrobbleTime,
							   'trackDuration' => $trackDuration,
							   'album'         => $album,
							   'trackNumber'   => $trackNumber,
							   'source'        => $source,
							   'rating'        => $rating,
							   'mbTrackId'     => $mbTrackId
							   );
		return true;
	}

	/**
	 * Submission process
	 *
	 * @throws md_Scrobbler_Exception
	 * @return boolean
	 */
	public function submit()
	{
		if(empty($this->queue))
		{
			throw new md_Scrobbler_Exception('Nothing to submit.');
			return false;
		}
		if(empty($this->sessionId) or $this->submitFailures > 2)
		{
			$this->handShake();
		}
		$curl = curl_init();
		curl_setopt($curl, CURLOPT_TIMEOUT, self::TIMEOUT);
		curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, self::TIMEOUT);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
		curl_setopt($curl, CURLOPT_URL, $this->submissionUrl);
		curl_setopt($curl, CURLOPT_POSTFIELDS, $this->generatePostData());
		$data = curl_exec($curl);
		curl_close ($curl);

		$data = explode("\n", $data);
		if($data[0] != 'OK')
		{
			$this->submitFailures++;
			if($data[0] == 'BADSESSION')
			{
				throw new md_Scrobbler_Exception('Bad session id.');
			}
			else
			{
				throw new md_Scrobbler_Exception('Submission failed : ' . $data[0]);
			}
			return false;
		}
		else
		{
			$this->queue = array();
			return true;
		}
		return false;
	}

	private function handShake()
	{
		if(empty($this->user) or empty($this->password))
		{
			throw new md_Scrobbler_Exception('Authentification credentials missing.');
			return false;
		}
		$curl = curl_init($this->generateScrobblerUrl());
		curl_setopt($curl, CURLOPT_TIMEOUT, self::TIMEOUT);
		curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, self::TIMEOUT);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);

		$data = curl_exec($curl);

		curl_close($curl);
		$data = explode("\n", $data);
		if($data[0] != 'OK')
		{
			$this->handShakeFailure = time();
			switch($data[0])
			{
				case 'BANNED':
					throw new md_Scrobbler_Exception('Client banned.');
					break;
				case 'BADTIME':
					throw new md_Scrobbler_Exception('Wrong system clock.');
					break;
				case 'BADAUTH':
					throw new md_Scrobbler_Exception('Wrong credentials.');
					break;
				default:
					throw new md_Scrobbler_Exception('Unexpected handshake error: ' . $data[0]);
					break;
			}
			return false;
		}
		else
		{
			$this->sessionId        = trim($data[1]);
			$this->nowPlayingUrl    = trim($data[2]);
			$this->submissionUrl    = trim($data[3]);
			$this->handShakeFailure = 0;
			$this->submitFailures   = 0;
			return true;
		}

		return false;
	}

	private function generateScrobblerUrl()
	{
		$stamp = time();
		return str_replace(
						   array('<client-id>',
								 '<client-ver>',
								 '<user>',
								 '<timestamp>',
								 '<auth>'),
						   array(self::clientId,
								 self::clientVer,
								 $this->user,
								 $stamp,
								 md5(md5($this->password) . $stamp)),
						   self::SCROBBLER_URL
						   );
	}

	private function generatePostData()
	{
		$body = 's=' . $this->sessionId . '&';
		$i = 0;
		foreach($this->queue as $item)
		{
			$body .= 'a[' . $i . ']=' . urlencode($item['artist']) . '&'
			. 't[' . $i . ']=' . urlencode($item['track']) . '&'
			. 'i[' . $i . ']=' . $item['scrobbleTime'] . '&'
			. 'o[' . $i . ']=' . $item['source'] . '&'
			. 'r[' . $i . ']=' . $item['rating'] . '&'
			. 'l[' . $i . ']=' . $item['trackDuration'] . '&'
			. 'b[' . $i . ']=' . urlencode($item['album']) . '&'
			. 'n[' . $i . ']=' . $item['trackNumber'] . '&'
			. 'm[' . $i . ']=' . $item['mbTrackId'] . '&';
			$i++;
		}

		return $body;
	}
}

Les suggestions Google

Il y a une fonctionnalitée sympa chez Google… les suggestions :

le nain

Surtout… ne me demandez pas pourquoi.

PHP5 et type hinting

PHP permet le “type hinting” mais uniquement pour les objets et les tableaux. Voici une bouine trouvée sur php.net pour ajouter à cette fonctionnalité les types scalaires, en attendant une hypothétique modification de PHP en ce sens :

 'is_bool',
        'integer'   => 'is_int',
        'float'     => 'is_float',
        'string'    => 'is_string',
        'resource'  => 'is_resource'
    );

    public static function initializeHandler()
    {

        set_error_handler('Typehint::handleTypehint');

        return TRUE;
    }

    private static function getTypehintedArgument($ThBackTrace, $ThFunction, $ThArgIndex, &$ThArgValue)
    {

        foreach ($ThBackTrace as $ThTrace)
        {
            if (isset($ThTrace['function']) && $ThTrace['function'] == $ThFunction)
            {

                $ThArgValue = $ThTrace['args'][$ThArgIndex - 1];

                return TRUE;
            }
        }

        return FALSE;
    }

    public static function handleTypehint($ErrLevel, $ErrMessage)
    {
        if ($ErrLevel == E_RECOVERABLE_ERROR)
        {
            if (preg_match(TYPEHINT_PCRE, $ErrMessage, $ErrMatches))
            {
                list($ErrMatch, $ThArgIndex, $ThClass, $ThFunction, $ThHint, $ThType) = $ErrMatches;
                if (isset(self::$Typehints[$ThHint]))
                {

                    $ThBacktrace = debug_backtrace();
                    $ThArgValue  = NULL;

                    if (self::getTypehintedArgument($ThBacktrace, $ThFunction, $ThArgIndex, $ThArgValue))
                    {
                        if (call_user_func(self::$Typehints[$ThHint], $ThArgValue))
                        {
                            return TRUE;
                        }
                    }
                }
            }
        }

        return FALSE;
    }
}

Typehint::initializeHandler();

function testInt(integer $int)
{
	return $int;
}

function testString(string $string)
{
	return $string;
}

testInt(5454);

testString('');

Consommez bien, on s’occupe de tout.

Je suis content de savoir que le gouvernement veille sur nous. Je suis vraiment rassuré. Il s’agit bien sur de “protéger les utilisateurs d’Internet”, alors forcément ça ne peut pas être mal.

Je pense qu’il est temps de revoir certains classiques

FirePHP

Une extension Firefox dénichée par l’ami Nico, FirePHP permet d’afficher des debugs directement dans la console Firebug, depuis un script PHP, tel qu’on le ferait en Javascript avec console.log(), console.dir(), etc.

La partie serveur trouvera naturellement sa place dans /usr/share/php.

Utilisation:

<?php
ob_start();
require('FirePHPCore/fb.php');

fb('Log message'  ,FirePHP::LOG);
fb('Info message' ,FirePHP::INFO);
fb('Warn message' ,FirePHP::WARN);
fb('Error message',FirePHP::ERROR);

fb('Message with label','Label',FirePHP::LOG);

fb(array('key1'=>'val1',
'key2'=>array(array('v1','v2'),'v3')),
'TestArray',FirePHP::LOG);

function test($Arg1) {
throw new Exception('Test Exception');
}
try {
test(array('Hello'=>'World'));
} catch(Exception $e) {
/* FirePHP peut aussi afficher une exception avec la stack trace */
fb($e);
}

/* Will show only in "Server" tab for the request */
fb(apache_request_headers(),
'RequestHeaders',FirePHP::DUMP);

echo('pouet');

Résultat:

Partenariat Zend et Dojo

Je l’imagine comme une conséquence du partenariat entre Zend et IBM: le Zend Framework sera livré avec Dojo et mettra l’accent sur l’échange de données en JSON.
J’aurai préféré de loin JQuery (visiblement je ne suis pas le seul !) mais il faut croire que Dojo se prête mieux à l’écriture d’helpers générant du code javascript cradingue.

Bon… je m’emporte. Ce sera peut-être pas si mal finalement. Wait and see.

Changement de dédiboite

En service depuis 2006, je viens de changer ma dedibox v1 contre une dedibox v2 flambant neuve. Et toujours sous Debian Etch bien sur. :-)

Ajax sous Django : Python et Taconite

Parce que je vais en avoir besoin pour faire de l’Ajax sous Django (en association avec http_response), j’ai récris en Python ma classe PHP servant à construire les “command documents”, soit les fichiers XML contenant les modification du DOM à apporter par le plugin Taconite de JQuery, en me basant sur minidom que l’on pourrait présenter comme étant l’équivalent de simpleXML en PHP.

Un peu d’indulgence, pour l’instant je suis encore un touriste dans le monde de Python (conseils bienvenus toutefois):

# usage:
#
# t = Taconite()
#
# t.append("#toto","")
# t.remove("#tutu")
# t.js('alert("hello world");')
# t.toggleClass('blue','body')
# t.css("body","background-color","white")
# [...]
# print t.toprettyxml()

import xml.dom.minidom as dom

class Taconite(dom.Document):
  def __init__(self):
    dom.Document.__init__(self)
    taconite = self.createElement("taconite")
    self.appendChild(taconite)

  def __str__(self):
    return self.toxml(encoding="utf-8")

  def camelizeCssProperty(self,property):
    words = property.split("-")
    camelized = words[0].lower()
    for word in words[1:] :
      camelized = camelized + word[0].upper() + word[1:]
    return camelized

  def js(self,script):
    command = self.createElement("eval")
    js = self.createTextNode(script)
    command.appendChild(js)
    self.childNodes[0].appendChild(command)

  def changeContentCommand(self,method,selector,content):
    html_dom = dom.parseString(content)
    command = self.createElement(method)
    command.setAttribute("select",selector)
    command.appendChild(html_dom.childNodes[0])
    self.childNodes[0].appendChild(command)

  def changeStateCommand(self,action,selector):
    command = self.createElement(action)
    command.setAttribute("select",selector)
    self.childNodes[0].appendChild(command)

  def CssCommand(self,action,css_class,selector):
    command1 = self.createElement(action)
    command1.setAttribute("select",selector)
    command1.setAttribute("arg1",css_class)
    command2 = self.createElement(action)
    command2.setAttribute("select",selector)
    command2.setAttribute("value",css_class)
    self.childNodes[0].appendChild(command1)
    self.childNodes[0].appendChild(command2)

  def addClass(self,css_class,selector):
    self.CssCommand("addClass",css_class,selector)

  def removeClass(self,css_class,selector):
    self.CssCommand("remove",css_class,selector)

  def toggleClass(self,css_class,selector):
    self.CssCommand("toggleClass",css_class,selector)

  def append(self,selector,content):
    self.changeContentCommand("append",selector,content)

  def prepend(self,selector,content):
    self.changeContentCommand("prepend",selector,content)

  def before(self,selector,content):
    self.changeContentCommand("before",selector,content)

  def after(self,selector,content):
    self.changeContentCommand("after",selector,content)

  def wrap(self,selector,content):
    self.changeContentCommand("wrap",selector,content)

  def replace(self,selector,content):
    self.changeContentCommand("replace",selector,content)

  def replaceContent(self,selector,content):
    self.changeContentCommand("replaceContent",selector,content)

  def remove(self,selector):
    self.changeStateCommand("remove",selector)

  def show(self,selector):
    self.changeStateCommand("show",selector)

  def hide(self,selector):
    self.changeStateCommand("hide",selector)

  def removeContent(self,selector):
    self.changeStateCommand("empty",selector)

  def css(self,selector,property,value):
    command = self.createElement("css")
    command.setAttribute("select",selector)
    command.setAttribute("name",self.camelizeCssProperty(property))
    command.setAttribute("value",value)
    self.childNodes[0].appendChild(command)

Quand j’aurai un peu de temps je m’occuperai des commentaires au format pydoc. En attendant, JQuery + Django… ça devrait faire des étincelles.

Python toujours…

Plus je creuse le sujet, plus je trouve Python et son écosystème très intéressant.

Le langage d’abord, souvent décrit comme concis et élégant est effectivement agréable, lisible et plein d’astuce sans jamais être obscure. Un programme Python est toujours bien présenté… d’ailleurs un programme Python mal indenté est un programme qui ne fonctionne pas !
Les possibilités du langage, objet de pied en cape, sont aussi énormes.
Ensuite, un peu à l’image de PHP, Python est fourni en standard avec un nombre impressionnant de modules si bien qu’il est inutile de courir après telle ou telle bibliothèque pour commencer à l’utiliser pleinement sur des cas réels, que ce soit en environnement web ou desktop.

L’écosystème ensuite, qui visiblement se porte bien: les frameworks de haut niveau pour le web ne manquent pas et c’est Python qui a été retenu par Google pour sa plateforme d’hébergement d’applications webs (comme à beaucoup d’autres endroits d’ailleurs). La communauté de développeurs, si elle n’est pas aussi importante que d’autres langages, semble toutefois très active, plus compétente, passionnée et très pédagogue !

Évidemment mon intérêt pour Python tient pour beaucoup à Django mais il est vrai aussi que sur le papier Rails offre les mêmes avantages, et pourtant je n’avais pas accroché… la faute à Ruby sans doute, ou peut-être à la documentation de l’époque.

Bref, je vais mettre de coté un moment Java-le-pas-très-fun (mais j’y reviendrai, soif de savoir et Jython oblige) pour me consacrer beaucoup plus à Python (et Django). Pour m’aider à intégrer ce langage dans une démarche méthodologique globale, j’ai trouvé un très bon livre chez Dunod, qui viendra compléter mon achat précédent :

Python, petit guide à l'usage du développeur agile

Écrit par Tarek Ziadé, évidemment spécialiste de la question, ce petit livre au ton parfois décalé passe très vite sur la syntaxe de Python pour faire la part belle aux méthodologies de développement agile avec ce langage : design patterns, tests unitaires, tests fonctionnels, développement dirigé par la documentation, gestion de projet, etc…

Ce livre traite de l’agilité appliquée à Python. Il est le fruit de dix ans de pratique.

De fait, on jurerait que Python a été taillé pour l’agilité :-)