//-*- Mode: C++; indent-tabs-mode: nil; -*-

//  BMP
//  Copyright (C) 2005-2007 BMP development.
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License Version 2
//  as published by the Free Software Foundation.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//
//  --
//
//  The BMPx project hereby grants permission for non-GPL compatible GStreamer
//  plugins to be used and distributed together with GStreamer and BMPx. This
//  permission is above and beyond the permissions granted by the GPL license
//  BMPx is covered by.

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif //HAVE_CONFIG_H

#include <gtk/gtk.h>
#include <gtkmm.h>
#include <glibmm.h>
#include <glibmm/i18n.h>

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cctype>
#include <iostream>
#include <fstream>
#include <vector>

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <strings.h>

#include <boost/format.hpp>
#include <boost/regex.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/regex.hpp>

#include "core.hh"

#include "bmp/bmp.hh"

#include "main.hh"
#include "base64.h"
#include "debug.hh"
#include "md5.h"
#include "paths.hh"
#include "network.hh"
#include "stock.hh"
#include "uri.hh"
#include "util.hh"
#include "xml.hh"

#include "lastfm.hh"

#include "lastfm-parsers.hh"
#include "lastfm-tags-libxml2-sax.hh"
#include "xspf-libxml2-sax.hh"

using namespace Gtk;
using namespace Glib;
using namespace Markup;
using namespace std;
using namespace Bmp;
using boost::algorithm::split;
using boost::algorithm::split_regex;
using boost::algorithm::is_any_of;
using boost::algorithm::find_nth;
using boost::algorithm::replace_all;
using boost::algorithm::trim;
using boost::iterator_range;

#define BMP_AS_USERAGENT      "BMP2"
#define BMP_LASTFMRADIO_VERSION   "2.0.0.0"
#define BMP_LASTFMRADIO_PLATFORM  "linux"

namespace
{
  typedef std::map < std::string, std::string > StringMap;

  StringMap 
  parse_to_map (std::string const& buffer)
  {
    using namespace Bmp;

    StringMap map;
    StrV lines;
    split_regex (lines, buffer, boost::regex ("\\\r?\\\n"));

    for (StrV::size_type n = 0; n < lines.size(); ++n)
    {
      iterator_range<std::string::iterator> match = find_nth (lines[n], "=", 0);
      if (match)
      {
        map[std::string(lines[n].begin(), match.begin())] = std::string (match.end(), lines[n].end()); 
      }
    }
    return map;
  }

  bool
  startsWith (std::string const& source, std::string const& target)
  {
    if (source.length() < target.length())
      return false;

    return (source.substr(0, target.length()) == target);
  }
}

namespace Bmp
{
  namespace LastFM
  {
    bool operator< (TagRankP const& a, TagRankP const& b)
    {
      return (a.second > b.second);
    }

    bool operator< (Tag const& a, Tag const& b)
    {
      return (a.name < b.name);
    }

    namespace XMLRPC
    {
      ustring
      formatXmlRpc (ustring const& method, xmlRpcVariantV const& argv)
      {
        xmlDocPtr doc (xmlNewDoc (BAD_CAST "1.0"));
        xmlNodePtr n_methodCall, n_methodName, n_params;

        n_methodCall = xmlNewNode (0, BAD_CAST "methodCall");
        xmlDocSetRootElement (doc, n_methodCall);

        n_methodName = xmlNewTextChild (n_methodCall, 0, BAD_CAST "methodName", BAD_CAST method.c_str());
        n_params = xmlNewChild (n_methodCall, 0, BAD_CAST "params", 0);

        for (xmlRpcVariantV::const_iterator i = argv.begin(); i != argv.end(); ++i)
        {
          xmlNodePtr n_param, n_value;
          n_param = xmlNewChild (n_params, 0, BAD_CAST "param", 0);
          n_value = xmlNewChild (n_param, 0, BAD_CAST "value", 0);

          xmlRpcVariant const& v (*i);

          switch (v.which())
          {
            case 0:
            {
              xmlNewTextChild (n_value, 0, BAD_CAST "string", BAD_CAST (boost::get <ustring> (v)).c_str());
              break;
            }

            case 1:
            {
              xmlNodePtr n_array (xmlNewChild (n_value, 0, BAD_CAST "array", 0)), n_data (xmlNewChild (n_array, 0, BAD_CAST "data", 0));
              UStrV const& strv (boost::get < ::Bmp::UStrV > (v));
              for (UStrV::const_iterator i = strv.begin() ; i != strv.end(); ++i)
              {
                xmlNodePtr n_data_value (xmlNewChild (n_data, 0, BAD_CAST "value", 0)),
                           G_GNUC_UNUSED n_data_string (xmlNewTextChild (n_data_value, 0, BAD_CAST "string", BAD_CAST (*i).c_str()));
              }
              break;
            }
          }
        }

        xmlKeepBlanksDefault (0);
        xmlChar * xmldoc = 0;
        int doclen = 0;
        xmlDocDumpFormatMemoryEnc (doc, &xmldoc, &doclen, "UTF-8", 1);
        ustring doc_utf8 ((const char*)(xmldoc));
        g_free (xmldoc);
        return doc_utf8;
      }

      /////////////////////////////////////////////////
      ///// Requests
      /////////////////////////////////////////////////

      XmlRpcCall::XmlRpcCall ()
      {
        m_soup_request = Soup::Request::create ("http://ws.audioscrobbler.com/1.0/rw/xmlrpc.php", true) ;
        m_soup_request->add_header("User-Agent", "BMP-2.0");
        m_soup_request->add_header("Accept-Charset", "utf-8");
        m_soup_request->add_header("Connection", "close");
      }
      void
      XmlRpcCall::setMethod (const ustring &method)
      { 
        ustring xmlData = formatXmlRpc (method, m_v);
        m_soup_request->add_request ("text/xml", xmlData);
      }
      void
      XmlRpcCall::cancel ()
      {
        m_soup_request.clear();
      }
      XmlRpcCall::~XmlRpcCall ()
      {
      }


      XmlRpcCallSync::XmlRpcCallSync ()
      {
        m_soup_request = Soup::RequestSync::create ("http://ws.audioscrobbler.com/1.0/rw/xmlrpc.php", true) ;
        m_soup_request->add_header("User-Agent", "BMP-2.0");
        m_soup_request->add_header("Accept-Charset", "utf-8");
        m_soup_request->add_header("Connection", "close");
      }
      void
      XmlRpcCallSync::setMethod (const ustring &method)
      { 
        ustring xmlData = formatXmlRpc (method, m_v);
        m_soup_request->add_request ("text/xml", xmlData);
      }
      XmlRpcCallSync::~XmlRpcCallSync ()
      {
      }


      ArtistMetadataRequest::ArtistMetadataRequest (ustring const& artist) 
      : XmlRpcCall ()
      , m_artist (artist)
      {
        m_v.push_back (m_artist);
        m_v.push_back (ustring ("en"));
        setMethod ("artistMetadata");
      }

      ArtistMetadataRequestRefPtr
      ArtistMetadataRequest::create (ustring const& artist)
      {
        return ArtistMetadataRequestRefPtr (new ArtistMetadataRequest (artist));
      }

      void
      ArtistMetadataRequest::run ()
      {
        m_soup_request->request_callback().connect (sigc::mem_fun (*this, &ArtistMetadataRequest::reply_cb));
        m_soup_request->run ();
      }

      void
      ArtistMetadataRequest::reply_cb (char const* data, guint size, guint status_code)
      {
        std::string chunk;

        xmlDocPtr             doc;
        xmlXPathObjectPtr     xpathobj;
        xmlNodeSetPtr         nv;

        doc = xmlParseMemory (data, size); 
        if (!doc)
        {  
          s_.emit (chunk, status_code);
          return;
        }

        xpathobj = xpath_query (doc, BAD_CAST "//member[name = 'wikiText']/value/string", NULL);
        if (!xpathobj)
        {
          s_.emit (chunk, status_code);
          return;
        }

        nv = xpathobj->nodesetval;
        if (!nv || !nv->nodeNr)
        {
          xmlXPathFreeObject (xpathobj);
          s_.emit (chunk, status_code);
          return;
        }

        xmlNodePtr node = nv->nodeTab[0];
        if (!node || !node->children)
        {
          xmlXPathFreeObject (xpathobj);
          s_.emit (chunk, status_code);
          return;
        }

        xmlChar * pcdata = XML_GET_CONTENT (node->children);

        if (pcdata)
        {
          chunk = (reinterpret_cast<char *>(pcdata));
          xmlFree (pcdata);
        }

        s_.emit (chunk, status_code);
      }



      RequestBase::RequestBase (ustring const& name)
      : m_name  (name)
      , m_pass  (mcs->key_get <std::string> ("lastfm", "password"))
      , m_user  (mcs->key_get <std::string> ("lastfm", "username"))
      , m_time  ((boost::format ("%llu") % time (NULL)).str())
      , m_key   (Util::md5_hex_string (m_pass.data(), m_pass.size()) + m_time) 
      , m_kmd5  (Util::md5_hex_string (m_key.data(), m_key.size()))
      {
        m_v.push_back (m_user);
        m_v.push_back (m_time);
        m_v.push_back (m_kmd5);
      }
  
      void
      RequestBase::run ()
      {
        setMethod (m_name);
        m_soup_request->run ();
      }



      TrackAction::TrackAction (ustring const& name, XSPF::Item const& item)
      : RequestBase (name)
      {
        m_v.push_back (item.creator);
        m_v.push_back (item.title);
      }



      TagAction::TagAction (ustring const& name, XSPF::Item const& item, UStrV const& strv)
      : RequestBase (name)
      {
        m_v.push_back (item.creator);

        if (m_name == "tagArtist")
        {
          /* nothing to do */
        }
    
        else

        if (m_name == "tagAlbum")
        {
          m_v.push_back (item.album);
        }

        else

        if (m_name == "tagTrack")
        {
          m_v.push_back (item.title);
        }

        m_v.push_back (strv);
        m_v.push_back (ustring ("append"));
      }


      RecommendAction::RecommendAction (RecommendItem item,
                                        ustring const& artist,
                                        ustring const& album,
                                        ustring const& title,
                                        ustring const& user,
                                        ustring const& message)
      : RequestBase ("recommendItem")
      {
        m_v.push_back (artist);

        if (item == RECOMMEND_ARTIST)
        {
          m_v.push_back (ustring());
          m_v.push_back ("artist");
        }

        else
        
        if (item == RECOMMEND_ALBUM)
        {
          m_v.push_back (album);
          m_v.push_back ("album");
        }

        else

        if (item == RECOMMEND_TRACK)
        {
          m_v.push_back (title);
          m_v.push_back ("track");
        }


        m_v.push_back (user);
        m_v.push_back (message);
        m_v.push_back ("en");
      }

    } // namespace:XMLRPC
  } // namespace:LastFM
} // namespace:Bmp

using namespace Bmp::LastFM::XMLRPC;

namespace Bmp
{
  namespace LastFM
  {
    Signal&
    Radio::signal_tuned ()
    {
      return Signals.Tuned;
    }

    Radio::SignalTuneError&
    Radio::signal_tune_error ()
    {
      return Signals.TuneError;
    }

    Radio::SignalPlaylist&
    Radio::signal_playlist ()
    {
      return Signals.Playlist;
    }

    Signal&
    Radio::signal_no_playlist ()
    {
      return Signals.NoPlaylist;
    }

    Signal&
    Radio::signal_disconnected()
    {
      return Signals.Disconnected;
    }

    Signal&
    Radio::signal_connected()
    {
      return Signals.Connected;
    }

    Radio::Radio ()
      : m_connected (false)
    {}

    Radio::~Radio ()
    {
      m_handshake_request.clear ();
    }

    bool
    Radio::connected () const
    {
      return m_connected;
    }

    void
    Radio::handshake_cb (char const * data, guint size, guint code)
    {
      if (code != 200)
      {
        m_connected = false;
        Signals.Disconnected.emit ();
        return;
      }

      std::string response (data, size);
      StringMap m = parse_to_map (response);
      StringMap::iterator iter_session = m.find ("session");

      if (iter_session != m.end () && iter_session->second == "FAILED")
      {
          m_connected = false;
          Signals.Disconnected.emit ();
          return;
      }

      m_session.session = iter_session->second;

      if (m.find ("subscriber") != m.end ())
          m_session.subscriber = bool(atoi(m.find ("subscriber")->second.c_str()));

      if (m.find ("base_url") != m.end ())
          m_session.base_url = m.find ("base_url")->second;

      if (m.find ("base_path") != m.end ())
          m_session.base_path = m.find ("base_path")->second;

      m_connected = (!m_session.session.empty() &&
                     !m_session.base_url.empty() &&
                     !m_session.base_path.empty());

      if (m_connected)
      {
        debug("lastfm", "%s: Radio Handshake OK. ID [%s], URL: [%s%s]",
          G_STRLOC,
          m_session.session.c_str(),
          m_session.base_url.c_str(),
          m_session.base_path.c_str()
        );
        Signals.Connected.emit ();
      }
      else
      {
        debug("lastfm", "%s: Radio Handshake unsucessful (session, url or url path are empty)", G_STRLOC );
        Signals.Disconnected.emit ();
      }
    }
    
    void
    Radio::disconnect ()
    {
      m_handshake_request.clear();
      m_connected = false;
      Signals.Disconnected.emit ();
    }

    void 
    Radio::handshake ()
    {
      if (!m_connected)
      {
        std::string user (mcs->key_get <std::string> ("lastfm", "username"));
        std::string pass (mcs->key_get <std::string> ("lastfm", "password"));
        std::string pmd5 (Util::md5_hex_string (pass.data(), pass.size()));

        std::string uri ((boost::format ("http://ws.audioscrobbler.com/radio/handshake.php?version=%s&platform=%s&username=%s&passwordmd5=%s")
                          % BMP_LASTFMRADIO_VERSION
                          % BMP_LASTFMRADIO_PLATFORM 
                          % user
                          % pmd5
                        ).str());

        URI u (uri, true); 
        m_handshake_request = Soup::Request::create (ustring (u));
        m_handshake_request->request_callback().connect (sigc::mem_fun (*this, &Radio::handshake_cb));
        m_handshake_request->run ();
      }
    }

    Radio::Session const& 
    Radio::session () const
    {
      return m_session;
    }

    void
    Radio::playurl_cb (char const * data, guint size, guint code, bool playlist) 
    {
      if (code != 200)
      {
        Signals.TuneError.emit ((boost::format (_( "Request Reply: %u")) % code).str());
        return;
      }

      if( playlist ) 
      {
        Signals.Tuned.emit ();
        parse_playlist (data, size);
        return;
      }
  
      std::string chunk;
      chunk.append (data, size);
      StringMap m (parse_to_map (chunk));

      if (m["response"] != std::string("OK"))
      {
        int error_code = g_ascii_strtoull (m["error"].c_str(), NULL, 10);
        std::string error_message;
        switch (error_code)
        {
            case LFM_ERROR_NOT_ENOUGH_CONTENT:
            {
                error_message = _( "There is not enough content to play this station." );
            }
            break;

            case LFM_ERROR_FEWGROUPMEMBERS:
            {
                error_message = _( "This group does not have enough members for radio." );
            }
            break;

            case LFM_ERROR_FEWFANS:
            {
                error_message = _( "This artist does not have enough fans for radio." );
            }
            break;

            case LFM_ERROR_UNAVAILABLE:
            {
                error_message = _( "This item is not available for streaming." );
            }
            break;

            case LFM_ERROR_SUBSCRIBE:
            {
                error_message = _( "This feature is only available to subscribers." );
            }
            break;

            case LFM_ERROR_FEWNEIGHBOURS:
            {
                error_message = _( "There are not enough neighbours for this radio." );
            }
            break;

            case LFM_ERROR_OFFLINE:
            {
                error_message = _( "The streaming system is offline for maintenance, please try again later." );
                m_connected = false;
                Signals.Disconnected.emit ();
            }
            break;

            default:
            {
                error_message = _( "Starting radio failed, Unknown Error (Last.fm Servers might be down)" );
                m_connected = false;
                Signals.Disconnected.emit ();
            }
        }
        Signals.TuneError.emit (error_message);
        return;
      }

      Signals.Tuned.emit ();
    }

    void
    Radio::playurl (std::string const& url)
    {
      if (!m_connected)
      {
        throw LastFMNotConnectedError(_("No current Last.fm session (please reconnect in the Preferences)"));
      }

      std::string request_uri;
      bool playlist = is_playlist (url);

      if( playlist ) 
      {
        request_uri =((boost::format ("http://%s/1.0/webclient/getresourceplaylist.php?sk=%s&url=%s&desktop=1")
                % m_session.base_url
                % m_session.session
                % URI::escape_string (url)).str()
        );
      }
      else
      {
        request_uri =((boost::format ("http://%s%s/adjust.php?session=%s&url=%s&lang=en")
                % m_session.base_url
                % m_session.base_path
                % m_session.session
                % URI::escape_string (url)).str()
        );
      }

      m_playurl_request = Soup::Request::create (request_uri);
      m_playurl_request->request_callback().connect (sigc::bind (sigc::mem_fun (*this, &Radio::playurl_cb), playlist));
      m_playurl_request->run ();
    }

    void
    Radio::playlist_cb (char const * data, guint size, guint code)
    {
      if (code == SOUP_STATUS_CANCELLED)
      {
            return;
      }

      if (code == 401) // invalid session
      {
            m_connected = false;
            Signals.Disconnected.emit ();
            Signals.NoPlaylist.emit ();
            return;
      }
    
      if (code == 503) // playlist service not responding
      {
            /* ... */
            Signals.NoPlaylist.emit ();
            return;
      }
     
      if (code == 200) 
      {
            parse_playlist (data, size);
            return;
      }
    }

    bool
    Radio::is_playlist (std::string const& url)
    {
      return (startsWith(url, "lastfm://play/") ||
              startsWith(url, "lastfm://preview/") ||
              startsWith(url, "lastfm://track/") ||
              startsWith(url, "lastfm://playlist/"));
    }

    void
    Radio::parse_playlist (char const* data, guint size)
    {
      XSPF::Playlist playlist;
      XSPF::XSPF_parse (playlist, data, size); 
      replace_all (playlist.Title, "+" , " ");
      trim (playlist.Title);
      playlist.Title = URI::unescape_string (playlist.Title);
      Signals.Playlist.emit( playlist );
    }
    
    void
    Radio::get_xspf_playlist_cancel ()
    {
      m_playlist_request.clear();
    }

    void
    Radio::get_xspf_playlist ()
    {
      std::string uri ((boost::format ("http://%s%s/xspf.php?sk=%s&discovery=%d&desktop=1.3.0.58") 
                        % m_session.base_url
                        % m_session.base_path 
                        % m_session.session 
                        % int (mcs->key_get<bool>("lastfm","discoverymode"))).str());

      m_playlist_request = Soup::Request::create (uri);
      m_playlist_request->request_callback().connect (sigc::mem_fun (*this, &Radio::playlist_cb));
      m_playlist_request->run ();
    }
  }
}

//-- Last.FM Submissions

#define BMP_AS_URI_HOST       "post.audioscrobbler.com"
#define BMP_AS_CLIENT_ID      "mpx"
#define BMP_AS_CLIENT_VERSION "2.0"

namespace Bmp
{
  namespace LastFM
  {
    Scrobbler::Scrobbler ()
    : m_queue_filename (build_filename (BMP_PATH_DATA_DIR, "protocol-1.2.lqm"))
    {
      if (file_test (m_queue_filename, FILE_TEST_EXISTS)) load_lqm (m_queue_filename);
      mcs->subscribe ("LastFM", "lastfm", "queue-enable", sigc::mem_fun (*this, &Scrobbler::mcs_queue_enable));
    }

    Scrobbler::~Scrobbler ()
    {
      m_handshake_request.clear ();
      m_now_playing_request.clear ();
      scrobble_request_cancel ();
      save_lqm (m_queue_filename);
    }

    void
    Scrobbler::load_lqm (std::string const& filename)
    {
      try{
          std::string data (file_get_contents (filename));
          LqmParser parser (m_queue);
          Markup::ParseContext context (parser);
          context.parse (data);
          context.end_parse ();
          parser.check_sanity ();
          debug("lastfm","%s: Loaded [%llu] items from the Last.fm queue.", G_STRLOC, guint64 (m_queue.size()));
        }
      catch (ConvertError & cxe)
        {
          g_warning( "%s: Glib Convert Error '%s'", G_STRLOC, cxe.what().c_str() );
        }
      catch (MarkupError & cxe)
        {
          g_warning( "%s: Glib Markup Error '%s'", G_STRLOC, cxe.what().c_str() );
        }
    }

    void
    Scrobbler::save_lqm (std::string const& filename)
    {
        Glib::Mutex::Lock L (m_queue_lock);

        xmlDocPtr doc = xmlNewDoc (BAD_CAST "1.0");

        xmlNodePtr lqm = xmlNewNode (0, BAD_CAST N_LQM);
        xmlSetProp (lqm, BAD_CAST "version", BAD_CAST "1.0");

        xmlNsPtr bmp = xmlNewNs (lqm, BAD_CAST XML_NS_BMP, BAD_CAST "bmp");
        xmlNodePtr q = xmlNewChild (lqm, bmp, BAD_CAST N_LQM_QUEUE, 0);

        xmlDocSetRootElement (doc, lqm);

        for (LQM::size_type n = 0 ; n < m_queue.size(); ++n)
        {
          static boost::format uint64_f ("%llu");
          static boost::format uint_f ("%u");

          TrackQueueItem const& i (m_queue[n]);

          xmlNodePtr n = xmlNewChild (q, bmp, BAD_CAST N_LQM_TRACK, 0);

          xmlSetProp (n, BAD_CAST "mbid",
                         BAD_CAST i.mbid.c_str());

          xmlSetProp (n, BAD_CAST "artist",
                         BAD_CAST i.artist.c_str());

          xmlSetProp (n, BAD_CAST "track",
                         BAD_CAST i.track.c_str());

          xmlSetProp (n, BAD_CAST "album",
                         BAD_CAST i.album.c_str());

          xmlSetProp (n, BAD_CAST "date",
                         BAD_CAST (uint64_f % i.date).str().c_str());

          xmlSetProp (n, BAD_CAST "length",
                         BAD_CAST (uint_f % i.length).str().c_str());

          xmlSetProp (n, BAD_CAST "nr",
                         BAD_CAST (uint_f % i.nr).str().c_str());

          xmlSetProp (n, BAD_CAST "source",
                         BAD_CAST i.source.c_str());

          xmlSetProp (n, BAD_CAST "rating",
                         BAD_CAST i.rating.c_str());
      }

      xmlCreateIntSubset (doc,
                          BAD_CAST "lqm",
                          NULL,
                          BAD_CAST "http://bmpx.backtrace.info/dtd/lqm-2.dtd" );

      xmlThrDefIndentTreeOutput (1);
      xmlKeepBlanksDefault (0);

      debug("lastfm","%s: Saving LQM to '%s'.", G_STRLOC, filename.c_str());
      (void)xmlSaveFormatFileEnc (filename.c_str(), doc, "utf-8", 1);

      debug("lastfm","%s: Saved [%llu] items to the Last.fm queue.", G_STRLOC, guint64 (m_queue.size()));
      xmlFreeDoc (doc);
    }

    void
    Scrobbler::mcs_queue_enable (MCS_CB_DEFAULT_SIGNATURE)
    {
      if (boost::get<bool>(value))
      {
            if (m_handshake_data.state != AS_HANDSHAKE_STATE_OK)
            {
                  handshake ();
            }
      }
      else
      {
                  m_handshake_data.state = AS_HANDSHAKE_STATE_UNDEFINED;
      }
    }

    bool
    Scrobbler::connected () const
    {
      return m_handshake_data.state == AS_HANDSHAKE_STATE_OK;
    }

    void 
    Scrobbler::handshake_cb (char const * data, guint size, guint code)
    {
      if (code != 200)
      {
        Signals.Disconnected.emit ();
        return;
      }

      std::string response;
      response.append (data, size);

      if (!response.empty())
      {
        enum StatusField
        {
          STATUS_CODE = 0,
          STATUS_INFO = 1,
        };

        enum ResponseLine
        {
          LINE_SESSION = 1,
          LINE_NP_URI = 2,
          LINE_SUBMISSION_URI = 3,
        };

        StrV  response_lines,
              status;

        split (response_lines, response, is_any_of("\n"));
        split (status, response_lines[0], is_any_of(" "));

        if (status[STATUS_CODE] == "OK")
        {
          m_handshake_data.session = response_lines[LINE_SESSION];
          m_handshake_data.np = response_lines[LINE_NP_URI];
          m_handshake_data.uri = response_lines[LINE_SUBMISSION_URI];
          m_handshake_data.state = AS_HANDSHAKE_STATE_OK;
        }
        else
        if (status[STATUS_CODE] == "BANNED")
        {
          m_handshake_data.state = AS_HANDSHAKE_BANNED;
        }
        else
        if (status[STATUS_CODE] == "BADAUTH")
        {
          m_handshake_data.state = AS_HANDSHAKE_BADAUTH;
        }
        else
        if (status[STATUS_CODE] == "BADTIME")
        {
          m_handshake_data.state = AS_HANDSHAKE_BADTIME;
        }
        else
        if (status[STATUS_CODE] == "FAILED")
        {
          m_handshake_data.info = status[STATUS_INFO];
          m_handshake_data.state = AS_HANDSHAKE_FAILED;
        }
      }

      if (m_handshake_data.state == AS_HANDSHAKE_STATE_OK)
      {
          g_message
          ("%s: Scrobbler Handshake OK. ID:'%s', URL:'%s', NP URL: '%s'",
            G_STRLOC,
            m_handshake_data.session.c_str (),
            m_handshake_data.uri.c_str (),
            m_handshake_data.np.c_str()
          );

          Signals.Connected.emit ();

          if (m_queue.size())
            scrobble_request_run ();

          return;
      }
      else
      {
        switch (m_handshake_data.state)
        {
          case AS_HANDSHAKE_STATE_OK:
            /* handled above, as an exception since all other cases fall through to
               disconnection and stopping the submission thread */
          break;

          case AS_HANDSHAKE_BANNED:
            Core::Obj()->status_push_message (_("Audioscrobbler: Client Banned"));
          break;

          case AS_HANDSHAKE_BADAUTH:
            Core::Obj()->status_push_message (_("Audioscrobbler: Invalid Credentials Provided!"));
          break;

          case AS_HANDSHAKE_BADTIME:
            Core::Obj()->status_push_message (_("Audioscrobbler: Host clock out of sync (Please adjust your computer's clock)"));
          break;

          case AS_HANDSHAKE_FAILED:
            Core::Obj()->status_push_message (_("Audioscrobbler: Handshake Failed: ") + ustring (m_handshake_data.info));
          break;

          case AS_HANDSHAKE_STATE_UNDEFINED:
            Core::Obj()->status_push_message (_("Audioscrobbler: Handshake Failed; Unknown Error (Server might be down, or bad HTTP reply)"));
          break;

          default:
            Core::Obj()->status_push_message (_("Audioscrobbler: Couldn't understand server's reply"));
          break;
        }
        Signals.Disconnected.emit ();
      }
    }

    void
    Scrobbler::disconnect ()
    {
      if( m_handshake_request )
      {
          m_handshake_request.clear();
      }

      m_handshake_data.state = AS_HANDSHAKE_STATE_UNDEFINED; 
      Signals.Disconnected.emit ();
    }

    void 
    Scrobbler::handshake ()
    {
      if (NM::Obj()->Check_Status())
      {
        std::string username (mcs->key_get<std::string>("lastfm", "username"));
        std::string password (mcs->key_get<std::string>("lastfm", "password"));

        if (username.empty () || password.empty ())
        {
          Signals.Disconnected.emit ();
          return;
        }

        if (m_handshake_data.state == AS_HANDSHAKE_STATE_OK)
        {
          return;
        }

        time_t          stamp     = time (NULL);
        std::string     auth1     = (boost::format ("%s%llu") 
                                      % Util::md5_hex_string (password.data(), password.size())
                                      % guint64 (stamp)
                                    ).str();
        std::string     auth2     = Util::md5_hex_string (auth1.data(), auth1.size());
        std::string     uri       = ((boost::format ("http://%s/?hs=true&p=1.2&c=%s&v=%s&u=%s&t=%llu&a=%s")
                                      % BMP_AS_URI_HOST
                                      % BMP_AS_CLIENT_ID 
                                      % BMP_AS_CLIENT_VERSION 
                                      % username 
                                      % stamp
                                      % auth2
                                    ).str());

        m_handshake_data.state = AS_HANDSHAKE_STATE_UNDEFINED;

        URI u (uri, true);
        m_handshake_request = Soup::Request::create (ustring (u));
        m_handshake_request->request_callback().connect (sigc::mem_fun (*this, &Scrobbler::handshake_cb));
        m_handshake_request->run ();
      }
      else
      {
        if (m_queue.size())
          scrobble_request_run ();
      }
    }

    void
    Scrobbler::scrobble_cb (char const * data, guint size, guint code, LQM::size_type n_items)
    {
      if (code == SOUP_STATUS_CANCELLED)
      {
	      debug("lastfm", "%s: scrobble cancelled.", G_STRLOC);
        goto scrobble_cb_exit;
      }

      // Server reply is HTTP OK, so let's see
      if (code == 200)
      {
            StrV response_lines;
            std::string response;
            response.append (data, size);
            split (response_lines, response, is_any_of("\n"));

            if (response_lines[0] == "BADSESSION")
            {
                  debug("lastfm", "%s: Submitting songs failed: %s", G_STRLOC, response_lines[0].c_str());
            }

            else

            if (response_lines[0].substr(0,6) == "FAILED")

            {
                  debug("lastfm", "%s: Submitting songs failed: %s", G_STRLOC, response_lines[0].c_str());
            }

            else 

            if (response_lines[0] == "OK")
            {
                  debug("lastfm", "%s: Submitting songs succeeded.", G_STRLOC);
                  m_queue.erase (m_queue.begin(), m_queue.begin() + n_items); // remove submitted songs from the queue
            }

      }
      else
      {
            g_warning ("%s: Network Error: %s", G_STRLOC, soup_status_get_phrase (code));
      }
    
      scrobble_cb_exit:
      save_lqm (m_queue_filename);
    }
 
    void
    Scrobbler::scrobble_request_cancel ()
    {
      if (m_scrobble_request)
      {
          m_scrobble_request.clear();
      }
    }

    void
    Scrobbler::scrobble_request_run ()
    {
      if (!NM::Obj()->Check_Status() || !connected())
      {
        save_lqm (m_queue_filename);
        return;
      }

      static boost::format
        f_queue_head ("s=%s&");
      static boost::format
        f_queue_item ("a[%u]=%s&t[%u]=%s&b[%u]=%s&n[%u]=%s&i[%u]=%u&o[%u]=%s&l[%u]=%u&m[%u]=%s&r[%u]=%s");

      /* We sort by timestamps as otherwise AS will blarf that we submitted stuff now that is dated earlier than
       * our later submission
       */
      std::sort (m_queue.begin(), m_queue.end());

      /* Create post data header
       */
      std::string postdata ((f_queue_head % m_handshake_data.session).str()); 

      /* "The submission body may contain the details for up to 50 tracks which are being submitted."
       * (http://www.audioscrobbler.net/development/protocol)
       */
      LQM::size_type n_items ((m_queue.size() >= 50) ? 50 : m_queue.size());

      for (LQM::size_type n = 0 ; n < n_items ; ++n)
      {
          TrackQueueItem const& item (m_queue[n]);
          postdata += ((f_queue_item  % n % URI::escape_string (item.artist)
                                      % n % URI::escape_string (item.track)
                                      % n % URI::escape_string (item.album)
                                      % n % ((item.nr > 0) ? (boost::format ("%u") % item.nr).str() : string())
                                      % n % item.date
                                      % n % item.source
                                      % n % item.length
                                      % n % item.mbid
                                      % n % item.rating).str());
          if (n != (n_items-1)) postdata += ("&");
      }

      m_scrobble_request = Soup::Request::create (m_handshake_data.uri, true);
      m_scrobble_request->add_header ("User-Agent", "BMP-2.0");
      m_scrobble_request->add_header ("Connection", "close");
      m_scrobble_request->add_request ("application/x-www-form-urlencoded", postdata);
      m_scrobble_request->request_callback().connect (sigc::bind (sigc::mem_fun (*this, &Scrobbler::scrobble_cb), n_items));
      m_scrobble_request->run ();
    }

    bool
    Scrobbler::scrobble_idle_tqi (TrackQueueItem const& item)
    {
      if (item.length < 30)
      {
        debug("lastfm", "%s: Not scrobbling, length is < 30 seconds", G_STRLOC );
        return false;
      }

      if (!(item.artist.size() && item.track.size()))
      {
        debug("lastfm", "%s: Not scrobbling, artist and title are missing", G_STRLOC );
        return false;
      }

      if (m_handshake_data.state != AS_HANDSHAKE_STATE_OK)
      {
        debug("lastfm","%s: Not scrobbling: handshake not OK (queueing)", G_STRLOC);
        save_lqm (m_queue_filename);
        return false;
      }

      scrobble_request_cancel ();
      m_queue.push_back (item);
      scrobble_request_run ();
      return false;
    }

    void
    Scrobbler::scrobble (TrackQueueItem const& item)
    {
      signal_idle().connect (sigc::bind (sigc::mem_fun (*this, &Scrobbler::scrobble_idle_tqi), item));
    }

    bool
    Scrobbler::scrobble_idle (XSPF::Item const& item)
    {
      TrackQueueItem x;
      x.artist  = item.creator;
      x.album   = item.album;
      x.track   = item.title;
      x.date    = time (NULL);
      x.length  = item.duration;
      x.source  = (boost::format ("L%s") % item.trackauth).str();
      x.rating  = item.rating;
      return scrobble_idle_tqi (x);
    }

    void
    Scrobbler::scrobble (XSPF::Item const& item)
    {
      signal_idle().connect (sigc::bind (sigc::mem_fun (*this, &Scrobbler::scrobble_idle), item));
    }

    void
    Scrobbler::now_playing_cb (char const * data, guint size, guint code)
    {
      if (code == SOUP_STATUS_CANCELLED)
      {
	      debug("lastfm", "%s: now-playing cancelled.", G_STRLOC);
        return;
      }

      if (code != 200) // HTTP 200: OK
      {
        Core::Obj()->status_push_message ((boost::format (_("Audioscrobbler (Now-Playing): Server Replied with code %u")) % code).str());
        return;
      }

      StrV response_lines;
      std::string response;
      response.append (data, size);
      split (response_lines, response, is_any_of("\n"));

      if (response_lines[0] == "BADSESSION")
      {
	      debug("lastfm", "%s: now-playing failed: %s", G_STRLOC, response_lines[0].c_str());
        Core::Obj()->status_push_message (_("Audioscrobbler: Bad session specified (Please go to Preferences / Last.fm and reconnect)"));
	    }
      else
      if (response_lines[0] == "OK")
	    {
	      debug("lastfm", "%s: now-playing succeeded.", G_STRLOC);
	    }
    }

    void 
    Scrobbler::now_playing (TrackQueueItem const& item)
    {
      signal_idle().connect( sigc::bind( sigc::mem_fun( *this, &Scrobbler::now_playing_idle ), item ) );
    }

    bool
    Scrobbler::now_playing_idle (TrackQueueItem const& item)
    {
      if( m_handshake_data.state == AS_HANDSHAKE_STATE_OK )
      {
        if (item.length < 30)
          return false;

        if (!(item.artist.size() && item.track.size()))
          return false;

        static boost::format f_item ("s=%s&a=%s&t=%s&b=%s&n=%s&l=%llu&m=%s");
        std::string postdata  ((f_item  % m_handshake_data.session
                                        % URI::escape_string (item.artist).c_str()
                                        % URI::escape_string (item.track).c_str()
                                        % URI::escape_string (item.album).c_str()
                                        % ((item.nr > 0) ? (boost::format ("%u") % item.nr).str() : std::string())
                                        % item.length
                                        % item.mbid).str());

        debug("lastfm","%s: now-playing data: '%s'", G_STRLOC, postdata.c_str());

        m_now_playing_request = Soup::Request::create (m_handshake_data.np, true);
        m_now_playing_request->add_header ("User-Agent", "BMP-2.0");
        m_now_playing_request->add_header ("Connection", "close");
        m_now_playing_request->add_request ("application/x-www-form-urlencoded", postdata);
        m_now_playing_request->request_callback().connect (sigc::mem_fun (*this, &Scrobbler::now_playing_cb));
        m_now_playing_request->run ();
      }

      return false;
    }
  }
}

namespace
{
  void
  tag_parse (Bmp::URI const& u, Bmp::LastFM::TagV & x)
  {
    try{
        Soup::RequestSyncRefP request = Soup::RequestSync::create (ustring (u));
        guint code = request->run ();
        if (code == 200)
        {
          Bmp::LastFM::WS::TagParser p (x);
          Markup::ParseContext context (p);
          context.parse (request->get_data());
          context.end_parse ();
        }
        else
        {
          g_warning( "%s: HTTP Reply: %u", G_STRLOC, code);
        }
      }
    catch (ConvertError& cxe)
      {
        g_warning ("%s: Glib Convert Error '%s'", G_STRLOC, cxe.what().c_str());
      }
   catch (MarkupError& cxe)
      {
        g_warning ("%s: Glib Markup Error '%s'", G_STRLOC, cxe.what().c_str());
      }
  }

  void
  user_parse (Bmp::URI const& u, Bmp::LastFM::UserV & x)
  {
    try{
        Soup::RequestSyncRefP request = Soup::RequestSync::create (ustring (u));
        guint code = request->run ();
        if (code == 200)
        {
          LastFM::WS::UserParser p (x);
          Markup::ParseContext context (p);
          context.parse (request->get_data());
          context.end_parse ();
        }
        else
        {
          g_warning( "%s: HTTP Reply: %u", G_STRLOC, code);
        }
      }
    catch (ConvertError& cxe)
      {
        g_warning( "%s: Glib Convert Error '%s'", G_STRLOC, cxe.what().c_str() );
      }
   catch (MarkupError& cxe)
      {
        g_warning( "%s: Glib Markup Error '%s'", G_STRLOC, cxe.what().c_str() );
      }
  }

  void
  artist_parse (Bmp::URI const& u, Bmp::LastFM::LastFMArtistV & x)
  {
    try{
        Soup::RequestSyncRefP request = Soup::RequestSync::create (ustring (u));
        guint code = request->run ();
        if (code == 200)
        {
          std::string data = request->get_data();
          g_print("Data: %s\n", data.c_str());
          LastFM::WS::ArtistParser p (x);
          Markup::ParseContext context (p);
          context.parse (data);
          context.end_parse ();
        }
        else
        {
          g_warning( "%s: HTTP Reply: %u", G_STRLOC, code );
        }
      }
    catch (ConvertError& cxe)
      {
        g_warning( "%s: Glib Convert Error '%s'", G_STRLOC, cxe.what().c_str() );
      }
   catch (MarkupError& cxe)
      {
        g_warning( "%s: Glib Markup Error '%s'", G_STRLOC, cxe.what().c_str() );
      }
  }
}

namespace Bmp
{
  namespace LastFM
  {
    namespace WS
    {
      TagsGlobReq::TagsGlobReq (TagGlobalType type, std::string const& id1, std::string const& id2)
      : m_soup_request (Soup::RequestRefP (0))
      {
        static boost::format
          global_tags_f1 ("http://ws.audioscrobbler.com/1.0/%s/%s/toptags.xml");

        static boost::format
          global_tags_f2 ("http://ws.audioscrobbler.com/1.0/%s/%s/%s/toptags.xml");

        ustring uri;

        switch (type)
        {
          case TAGS_GLOBAL_ARTIST:
            uri = ((global_tags_f1  % "artist" 
                                    % URI::escape_string (id1)).str());
            break;

          case TAGS_GLOBAL_TRACK:
            uri = ((global_tags_f2  % "track" 
                                    % URI::escape_string (id1)
                                    % URI::escape_string (id2)).str());
            break;
        }

        m_soup_request = Soup::Request::create (uri);
        m_soup_request->add_header("User-Agent", "BMP-2.0");
        m_soup_request->add_header("Accept-Charset", "utf-8");
        m_soup_request->add_header("Connection", "close");
      }

      void
      TagsGlobReq::run ()
      {
        m_soup_request->request_callback().connect (sigc::mem_fun (*this, &TagsGlobReq::reply_cb));
        m_soup_request->run ();
      }

      void
      TagsGlobReq::cancel ()
      {
        if (m_soup_request)
        {
          m_soup_request.clear();
        }
      }

      TagsGlobReq::~TagsGlobReq ()
      {}

      void
      TagsGlobReq::reply_cb (char const* data, guint size, guint status_code)
      {
        if (status_code == 200 && data && size) 
        {
          try{
              Bmp::LastFM::WS::TagParser p (m_tags);
              Markup::ParseContext context (p);
              context.parse (data, data+size);
              context.end_parse ();
            }
          catch (ConvertError & cxe)
            {
              g_warning( "%s: Glib Convert Error '%s'", G_STRLOC, cxe.what().c_str() );
            }
         catch (MarkupError & cxe)
            {
              g_warning( "%s: Glib Markup Error '%s'", G_STRLOC, cxe.what().c_str() );
            }
        }
        s_tags_.emit (m_tags);
      }

      TagsGlobReqRefP
      TagsGlobReq::create (TagGlobalType type, std::string const& id1, std::string const& id2) 
      {
        return TagsGlobReqRefP (new TagsGlobReq (type, id1, id2));
      }


      TagsUserReq::TagsUserReq (TagUserType type, std::string const& id1, std::string const& id2)
      {
        ustring uri ("http://ws.audioscrobbler.com/1.0/user/");
        uri += mcs->key_get <std::string> ("lastfm","username");

        switch (type)
        {
          case TAGS_USER_TOPTAGS:
              uri += "/tags.xml";
              break;

          case TAGS_USER_ARTIST:
              uri += "/artisttags.xml";
              uri += "?artist=";
              uri += id1;
              break;

          case TAGS_USER_ALBUM:
              uri += "/albumtags.xml";
              uri += "?artist=";
              uri += id1;
              uri += "&album=";
              uri += id2; 
              break;

          case TAGS_USER_TRACK:
              uri += "/tracktags.xml";
              uri += "?artist=";
              uri += id1;
              uri += "&track=";
              uri += id2;
              break;
        }

        m_soup_request = Soup::Request::create (uri);
        m_soup_request->add_header("User-Agent", "BMP-2.0");
        m_soup_request->add_header("Accept-Charset", "utf-8");
        m_soup_request->add_header("Connection", "close");
      }

      void
      TagsUserReq::run ()
      {
        m_soup_request->request_callback().connect (sigc::mem_fun (*this, &TagsUserReq::reply_cb));
        m_soup_request->run ();
      }

      TagsUserReq::~TagsUserReq ()
      {}

      void
      TagsUserReq::reply_cb (char const* data, guint size, guint status_code)
      {
        if (status_code == 200 && data && size) 
        {
          try{
              Bmp::LastFM::WS::TagParser p (m_tags);
              Markup::ParseContext context (p);
              context.parse (data, data+size);
              context.end_parse ();
            }
          catch (ConvertError & cxe)
            {
              g_warning( "%s: Glib Convert Error '%s'", G_STRLOC, cxe.what().c_str() );
            }
         catch (MarkupError& cxe)
            {
              g_warning( "%s: Glib Markup Error '%s'", G_STRLOC, cxe.what().c_str() );
            }
        }
        s_tags_.emit (m_tags);
      }

      TagsUserReqRefP
      TagsUserReq::create (TagUserType type, std::string const& id1, std::string const& id2) 
      {
        return TagsUserReqRefP (new TagsUserReq (type, id1, id2));
      }

      // Synchronous calls 
      void
      tags_glob ( TagGlobalType type,
                  TagV & tags,
                  std::string const& id1,
                  std::string const& id2)
      {
        static boost::format
          global_tags_f1 ("http://ws.audioscrobbler.com/1.0/%s/%s/toptags.xml");

        static boost::format
          global_tags_f2 ("http://ws.audioscrobbler.com/1.0/%s/%s/%s/toptags.xml");

        ustring uri;

        switch (type)
        {
          case TAGS_GLOBAL_ARTIST:
            uri = ((global_tags_f1  % "artist" 
                                    % URI::escape_string (id1)).str());
            break;

          case TAGS_GLOBAL_TRACK:
            uri = ((global_tags_f2  % "track" 
                                    % URI::escape_string (id1)
                                    % URI::escape_string (id2)).str());
            break;
        }

        Bmp::URI u (uri, true);
        tag_parse (u, tags);
      }

      void
      tags_user  ( TagUserType type,
                    TagV & tags,
                    std::string const& id1,
                    std::string const& id2)
      {
        ustring uri ("http://ws.audioscrobbler.com/1.0/user/");
        uri += mcs->key_get <std::string> ("lastfm","username");

        switch (type)
        {
          case TAGS_USER_TOPTAGS:
              uri += "/tags.xml";
              break;

          case TAGS_USER_ARTIST:
              uri += "/artisttags.xml";
              uri += "?artist=";
              uri += id1;
              break;

          case TAGS_USER_ALBUM:
              uri += "/albumtags.xml";
              uri += "?artist=";
              uri += id1;
              uri += "&album=";
              uri += id2; 
              break;

          case TAGS_USER_TRACK:
              uri += "/tracktags.xml";
              uri += "?artist=";
              uri += id1;
              uri += "&track=";
              uri += id2;
              break;
        }

        Bmp::URI u (uri, true);
        tag_parse (u, tags);
      }

      void
      users (UserType type, std::string const& username, UserV & x)
      {
        Bmp::URI u;
        switch (type)
        {
          case UT_F:
            u = Bmp::URI ((boost::format ("http://ws.audioscrobbler.com/1.0/user/%s/friends.xml") % username).str(), true);
            break; 

          case UT_N:
            u = Bmp::URI ((boost::format ("http://ws.audioscrobbler.com/1.0/user/%s/neighbours.xml") % username).str(), true);
            break; 
        }

        user_parse (u, x);
      }

      void
      artists (ArtistType type, std::string const& id, LastFMArtistV & x)
      {
        Bmp::URI u;

        switch (type)
        {
          case AT_USER_TOP:
            u = Bmp::URI ((boost::format ("http://ws.audioscrobbler.com/1.0/user/%s/topartists.xml") % id).str(), true);
            break;

          case AT_TAG_TOP:
            u = Bmp::URI ((boost::format ("http://ws.audioscrobbler.com/1.0/tag/%s/topartists.xml") % id).str(), true);
            break;

          case AT_SIMILAR:
            u = Bmp::URI ((boost::format ("http://ws.audioscrobbler.com/1.0/artist/%s/similar.xml") % id).str(), true);
            break;

          case AT_USER_SYSTEMREC:
            u = Bmp::URI ((boost::format ("http://ws.audioscrobbler.com/1.0/user/%s/systemrecs.xml") % id).str(), true);
            break;
        }
  
        artist_parse (u, x);
      }

      // Homebrewn algorithm to calculate a user's most matching tags based on his profile
      // Works quite well, but it could need some documentation to justify its workings.
      void
      matchtags (std::string const& username, RankedTagV & ranked)
      {
        typedef std::set <ustring> Keeper;

        LastFMArtistV a;
        artists (AT_USER_TOP, username, a);

        debug("lastfm","%s: %llu artists", G_STRLOC, guint64(a.size()));

        Keeper k; 
        for (LastFMArtistV::iterator i = a.begin(); i != a.end(); ++i)
        {
          LastFMArtist const& artist (*i);

          // We keep only artists with a ranking <= 25
          if (artist.rank <= 25)
          {
            // Now get the tags for this artist
            TagV v;
            tags_glob (TAGS_GLOBAL_ARTIST, v, artist.name);
           
            // We keep the first 10, and adjust their ranking depending on the artist's rank 
            TagV::size_type n = 0;
            for (TagV::const_iterator i = v.begin(); i != v.end(); ++i)
            {
              if (k.find (i->name) == k.end())
              {
                k.insert (i->name);

                LastFMArtistV tag_artists;
                artists (AT_TAG_TOP, i->name, tag_artists);

                // If the tag has been used only on <= 5 artists, we consider it not meaningful enough
                if (tag_artists.size() > 5)
                {
                  ranked.push_back (TagRankP (*i, i->count * (101-artist.rank)));
                }
              }

              if (++n == 5)
              {
                break;
              }

        } } } }

      std::string
      avatar_url (std::string const& username)
      {
        static boost::format
          url_f ("http://ws.audioscrobbler.com/1.0/user/%s/profile.xml");

        std::string uri ((url_f % username).str());
        Soup::RequestSyncRefP request = Soup::RequestSync::create (uri);
        guint code = request->run ();

        if (code != 200)
        {
          // md FIXME Throw exception
          return std::string();
        }

        std::string data = request->get_data (); 
        if (data.empty())
          return std::string();

        xmlDocPtr             doc;
        xmlXPathObjectPtr     xpathobj;
        xmlNodeSetPtr         nv;

        doc = xmlParseMemory ((const char*)(data.c_str()), strlen (data.c_str()));
        if (!doc)
          return std::string (); 

        xpathobj = xpath_query (doc, BAD_CAST "//avatar", NULL);
        if (!xpathobj)
          return std::string(); 

        nv = xpathobj->nodesetval;
        if (!nv || !nv->nodeNr)
        {
          xmlXPathFreeObject (xpathobj);
          return std::string(); 
        }

        std::string avatar;
        xmlChar * ch = XML_GET_CONTENT (nv->nodeTab[0]->children);
        avatar = (char*)(ch);
        xmlFree (ch);

        //xmlXPathFreeObject (xpathobj);
        //xmlFreeDoc (doc);  
    
        return avatar;
      }

    } // namespace:WS
  } // namespace:LastFM
} // namespace:Bmp
