/********************************************************************************
 *   Copyright (C) 2008-2009 by Bram Schoenmakers <bramschoenmakers@kde.nl>     *
 *                                                                              *
 *   This program is free software; you can redistribute it and/or modify       *
 *   it under the terms of the GNU General Public License as published by       *
 *   the Free Software Foundation; either version 2 of the License, or          *
 *   (at your option) any later version.                                        *
 *                                                                              *
 *   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.,                                            *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA .             *
 ********************************************************************************/

#include <QDateTime>
#include <QDir>
#include <QDomDocument>
#include <QTimer>

#include <KIO/Job>
#include <KMD5>
#include <KRun>
#include <KStandardDirs>

#include "flickr_engine.h"

using namespace Plasma;

static const char* s_flickr_auth = "http://www.flickr.com/services/auth/";
static const char* s_flickr_rest = "http://www.flickr.com/services/rest/";
// Flickr API key for Flickr On Plasma
static const char* s_apikey = "64dfce7e96302f412c7f0ece0897b1d2";
static const char* s_secret = "d7458fd6f08296e1";

static QHash< QString, PhotoListType >  s_photoListTypes;

FlickrEngine::FlickrEngine( QObject *p_parent, const QVariantList &p_args )
: DataEngine( p_parent, p_args )
, m_waiting_for_retrieval( false )
, m_timer( 0 )
, m_type( INTERESTINGNESS )
{
  KGlobal::locale()->insertCatalog("flickrop");

  s_photoListTypes[ "interestingness" ] = INTERESTINGNESS;
  s_photoListTypes[ "favorites" ] = FAVORITES;
  s_photoListTypes[ "photoset" ] = PHOTOSET;
  s_photoListTypes[ "tag" ] = TAG;
  s_photoListTypes[ "location" ] = LOCATION;
}

FlickrEngine::~FlickrEngine()
{
}

void FlickrEngine::init()
{
  m_timer = new QTimer( this );
  connect( m_timer, SIGNAL( timeout() ), this, SLOT( nextPhoto() ) );
  m_timer->setSingleShot( true );
  setData( "flickr", "" );
  setData( "error", "" );
  setData( "authentication", "" );
  setData( "clusters", "" );
  setData( "nsid", "" );
  setData( "photosets", "" );

  // in 5 minutes, clean the cache. we'll wait with this because this is called when the machine is still launching the desktop.
  QTimer::singleShot( 5 * 60 * 1000, this, SLOT( cleanCache() ) );
  // and for those machines with long uptimes, set a daily timer to clean the cache
  QTimer *cleanupTimer = new QTimer( this );
  connect( cleanupTimer, SIGNAL( timeout() ), this, SLOT( cleanCache() ) );
  cleanupTimer->start( 24 * 60 * 60 * 1000 );

  // seed the RNG
  qsrand( QDateTime::currentDateTime().time().msec() );
}

bool FlickrEngine::processResponse( KJob *p_job, QDomDocument *p_doc )
{
  if ( p_job->error() )
  {
    setError( i18n( "An error occured while obtaining data from Flickr." ) );
  }
  else
  {
    KIO::StoredTransferJob *job = qobject_cast<KIO::StoredTransferJob*>( p_job );
    if ( job )
    {
      p_doc->setContent( job->data() );
      return true;
    }
  }

  return false;
}

QString FlickrEngine::signCall( const Params &p_arguments )
{
  // Params is a QMap, the keys are sorted, just like Flickr Auth wants it.
  QString signature = s_secret;
  Params::const_iterator it = p_arguments.begin();
  for ( ; it != p_arguments.end(); ++it )
  {
    signature += it.key();
    signature += it.value();
  }
  KMD5 md5( signature.toUtf8() );
  return QString::fromUtf8( md5.hexDigest().data() );
}

KUrl FlickrEngine::constructApiUrl( Params &p_arguments, bool p_signed )
{
  // add the API key
  p_arguments["api_key"] = s_apikey;

  Params::const_iterator it;
  if ( p_signed )
  {
    p_arguments["api_sig"] = signCall( p_arguments );
  }

  KUrl url( s_flickr_rest );
  it = p_arguments.begin();
  for ( ; it != p_arguments.end(); ++it )
  {
    url.addQueryItem( it.key(), it.value() );
  }

  return url;
}

void FlickrEngine::startAuthentication()
{
  Params p;
  p["method"] = "flickr.auth.getFrob";
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, true ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob* ) ), this, SLOT( frobJobDone( KJob * ) ) );
}

void FlickrEngine::frobJobDone( KJob *p_job )
{
  if ( p_job->error() )
  {
    setError( i18n( "Could not initiate authentication." ) );
  }
  else
  {
    // obtain frob
    KIO::StoredTransferJob *job = qobject_cast<KIO::StoredTransferJob*>( p_job );
    QByteArray data = job->data();
    QDomDocument doc;
    doc.setContent( job->data() );
    m_frob = doc.elementsByTagName( "frob" ).item(0).toElement().text();

    // sign it
    Params p;
    p["api_key"] = s_apikey;
    p["perms"] = "write";
    p["frob"] = m_frob;
    QString sig = signCall( p );

    // now, construct the URL and run the browser on it
    KUrl url( s_flickr_auth );
    url.addQueryItem( "api_key", s_apikey );
    url.addQueryItem( "perms", "write" );
    url.addQueryItem( "frob", m_frob );
    url.addQueryItem( "api_sig", signCall( p ) );
    KRun::runUrl( url, "text/html", 0 );
    // show message box
  }
}

void FlickrEngine::continueAuthentication()
{
  // get token
  Params p;
  p["method"] = "flickr.auth.getToken";
  p["frob"] = m_frob;
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, true ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob * ) ), this, SLOT( tokenJobDone( KJob * ) ) );
}

void FlickrEngine::tokenJobDone( KJob *p_job )
{
  QDomDocument doc;
  if ( processResponse( p_job, &doc ) )
  {
    m_token = doc.elementsByTagName( "token" ).item(0).toElement().text();
    setData( "authentication", "token", m_token );
    setData( "authentication", "token_status", true );
  }
}

void FlickrEngine::checkToken()
{
  Params p;
  p["method"] = "flickr.auth.checkToken";
  p["auth_token"] = m_token;
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, true ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob* ) ), this, SLOT( tokenCheckDone( KJob * ) ) );
}

void FlickrEngine::tokenCheckDone( KJob *p_job )
{
  QDomDocument doc;
  if ( processResponse( p_job, &doc ) )
  {
    setData( "authentication", "token_status", doc.elementsByTagName( "err" ).isEmpty() );
    m_nsid = doc.elementsByTagName( "user" ).at( 0 ).toElement().attribute( "nsid" );
  }
}

void FlickrEngine::retrievePhotos()
{
  switch ( m_type )
  {
    case INTERESTINGNESS: retrieveInterestingnessList(); break;
    case FAVORITES: retrieveUserFavoriteList(); break;
    case PHOTOSET: retrievePhotoset( m_config["photoset-id"].toString() ); break;
    case TAG: retrievePhotosByCluster( m_config["tag"].toString(), m_config["cluster-id"].toString() ); break;
    case LOCATION: retrievePhotosByGeo( m_config["location-lon" ].toDouble(), m_config["location-lat"].toDouble(), m_config["location-accuracy"].toInt() ); break;
    default: retrieveInterestingnessList(); break;
  }
}

void FlickrEngine::retrieveInterestingnessList()
{
  Params p;
  p["method"] = "flickr.interestingness.getList";
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, false ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob* ) ), this, SLOT( listJobDone( KJob * ) ) );
}

void FlickrEngine::retrieveUserFavoriteList()
{
  Params p;
  p["method"] = "flickr.favorites.getList";
  p["auth_token"] = m_token;
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, true ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob* ) ), this, SLOT( listJobDone( KJob * ) ) );
}

void FlickrEngine::listJobDone( KJob *p_job )
{
  m_waiting_for_retrieval = false;

  QDomDocument doc;
  if ( processResponse( p_job, &doc ) )
  {
    processList( doc );
    // sometimes Flickr returns empty photo lists
    if ( m_photos.count() > 0 )
    {
      // retrieve first author
      if ( m_type != PHOTOSET )
      {
        retrieveAuthor( *m_photo_it );
      }
    }
  }
  nextPhoto();
}

void FlickrEngine::cleanCache()
{
  QDir d( KStandardDirs::locateLocal( "cache", "flickrop/" ), "*.jpg *.gif *.png", QDir::Time, QDir::Files );
  QDateTime aweekago = QDateTime::currentDateTime().addDays( -7 );

  QFileInfoList l = d.entryInfoList();
  foreach( const QFileInfo &fi, l )
  {
    if ( fi.created() < aweekago )
    {
      d.remove( fi.fileName() );
    }
  }
}

void FlickrEngine::processList( const QDomDocument &p_doc )
{
  // skip <?xml...
  QDomNode rsp = p_doc.firstChild().nextSibling();

  if ( rsp.isNull() || rsp.attributes().namedItem( "stat" ).toAttr().value() == "fail" )
  {
    setError( i18n( "Flickr returned an invalid response." ), ERR_APPLET );
  }
  else
  {
    m_photos.clear();

    QDomNodeList l = p_doc.elementsByTagName( "photo" );
    for ( uint i = 0; i < l.length(); ++i )
    {
      processPhoto( l.item( i ) );
    }

    // shuffle
    int size = m_photos.size();
    for ( int i = 0; i < size; ++i )
    {
      int rand = (int)( ( (double)(size - 1) * qrand() ) / RAND_MAX );
      m_photos.swap( i, rand );
    }

    m_photo_it = m_photos.begin();
  }
}

void FlickrEngine::processPhoto( const QDomNode &p_photo )
{
  Photo photo;

  QDomNamedNodeMap l = p_photo.attributes();
  QString id = l.namedItem( "id" ).toAttr().value();
  QString farm = l.namedItem( "farm" ).toAttr().value();
  QString server = l.namedItem( "server" ).toAttr().value();
  QString secret = l.namedItem( "secret" ).toAttr().value();
  // original secret

  photo.id = id;
  photo.url = constructPhotoURL( id, farm, server, secret );
  photo.title = l.namedItem( "title" ).toAttr().value();

  if ( m_type == PHOTOSET )
  {
    photo.author.username = m_config["photoset-username"].toString();
    photo.author.nsid = m_config["photoset-nsid"].toString();
  }
  else
  {
    photo.author.nsid = l.namedItem( "owner" ).toAttr().value();
  }

  photo.photoRetrieved = false;
  // no need to wait for the author retrieval when we're doing photosets. we already know the author and we won't try to retrieve the author.
  photo.authorRetrieved = m_type == PHOTOSET;

  QString path = "flickrop/";
  path += hashPhoto( photo );
  path += ".jpg";
  QString filename = KStandardDirs::locateLocal( "cache", path );
  photo.filename = filename;

  m_photos += photo;
}

KUrl FlickrEngine::constructPhotoURL( const QString &p_id, const QString &p_farm, const QString &p_server, const QString &p_secret )
{
  QString base = QString( "http://farm%1.static.flickr.com/%2/%3_%4" )
    .arg( p_farm )
    .arg( p_server )
    .arg( p_id )
    .arg( p_secret );
  if ( m_size == "o" )
  {
    // TODO: original secret?
  }
  else if ( !m_size.isEmpty() )
  {
    base += QString( "_%1" ).arg( m_size );
  }
  base += ".jpg";
  return KUrl( base );
}

QString FlickrEngine::hashPhoto( const Photo &p_photo )
{
  KMD5 hash( p_photo.url.url().toUtf8() );
  return QString::fromUtf8( hash.hexDigest().data() );
}

void FlickrEngine::retrieveAuthor( const Photo &p_photo )
{
  Params p;
  p["method"] = "flickr.people.getInfo";
  p["user_id"] = p_photo.author.nsid;
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, false ), KIO::NoReload, KIO::HideProgressInfo );
  job->setProperty( "photoid", p_photo.id );
  connect( job, SIGNAL( result( KJob* ) ), this, SLOT( authorJobDone( KJob * ) ) );
}

void FlickrEngine::authorJobDone( KJob *p_job )
{
  QDomDocument doc;
  if ( processResponse( p_job, &doc ) )
  {
    QString photoid( p_job->property( "photoid" ).toString() );
    QList<Photo>::iterator it;
    for ( it = m_photos.begin(); it != m_photos.end(); ++it )
    {
      if ( (*it).id == photoid )
      {
        break;
      }
    }

    if ( it == m_photos.end() )
    {
      // the photo disappeared, perhaps a new list was retrieved in the meantime
      return;
    }

    (*it).author.username = doc.elementsByTagName( "username" ).item(0).toElement().text();
    (*it).author.profileUrl = doc.elementsByTagName( "profileurl" ).item(0).toElement().text();
    (*it).author.photosUrl = doc.elementsByTagName( "photosurl" ).item(0).toElement().text();
    (*it).authorRetrieved = true;

    if ( (*it).photoRetrieved )
    {
      setPhoto( *it );
    }
    // otherwise wait for photoJobDone to finish
  }
}

void FlickrEngine::nextPhoto()
{
  // in case we force a photo update (i.e. the timer hasn't timed out yet)
  m_timer->stop();

  // the boolean makes sure to call this only once. for the frantics out there hitting Next Photo repeatedly.
  if ( !m_waiting_for_retrieval && m_photos.isEmpty() )
  {
    // retrieving the photos has failed somehow. let's try again in 5
    // minutes.
    QTimer::singleShot( 5 * 60 * 1000, this, SLOT( retrievePhotos() ) );
    m_waiting_for_retrieval = true; return;
  }
  else if ( m_waiting_for_retrieval )
  {
    // waiting, don't do anything now
    return;
  }

  if ( m_photo_it == m_photos.end() )
  {
    // we ran out of photos, retrieve new photos
    retrievePhotos();
    return;
  }

  if ( !KStandardDirs::exists( (*m_photo_it).filename ) )
  {
    // not in cache, retrieve it
    retrievePhoto( *m_photo_it );
  }
  else if ( (*m_photo_it).authorRetrieved )
  {
    // show photo from cache
    setPhoto( *m_photo_it );
  }
  else // photo is in cache, but we're waiting for the author job to complete
  {
    (*m_photo_it).photoRetrieved = true;
  }

  ++m_photo_it;
  m_timer->start( m_config.value("interval").toInt() * 1000 );

  // try to retrieve the next author in advance
  if ( m_photo_it != m_photos.end() && m_type != PHOTOSET )
  {
    retrieveAuthor( *m_photo_it );
  }
}

void FlickrEngine::retrievePhoto( const Photo &p_photo )
{
  KIO::StoredTransferJob *job = KIO::storedGet( p_photo.url, KIO::NoReload, KIO::HideProgressInfo );
  job->setProperty( "photoid", p_photo.id );
  connect( job, SIGNAL( result( KJob * ) ), SLOT( photoJobDone( KJob * ) ) );
}

void FlickrEngine::photoJobDone( KJob *p_job )
{
  if ( p_job->error() )
  {
    setError( i18n( "Could not retrieve the requested photo from Flickr." ), ERR_APPLET );
  }
  else
  {
    KIO::StoredTransferJob *job = qobject_cast<KIO::StoredTransferJob*>( p_job );

    // retrieve hash and reconstruct filename
    QString photoid = job->property( "photoid" ).toString();

    QList<Photo>::iterator it;
    for ( it = m_photos.begin(); it != m_photos.end(); ++it )
    {
      if ( (*it).id == photoid )
      {
        break;
      }
    }

    Q_ASSERT( it != m_photos.end() );

    // save data to cache
    QFile file( (*it).filename );
    file.open( QIODevice::WriteOnly );
    QDataStream s( &file );
    QByteArray ba = job->data();
    s.writeRawData( ba.constData(), ba.size() );
    file.close();
    (*it).photoRetrieved = true;

    if ( (*it).authorRetrieved )
    {
      setPhoto( *it );
    }
    // otherwise wait for authorJobDone to finish
  }
}

void FlickrEngine::setPhoto( const Photo &p_photo )
{
  QVariant data;
  data.setValue( p_photo );
  setData( "flickr", data );
}

void FlickrEngine::setError( const QString &p_error, ErrorType p_type )
{
  DataEngine::Data data;
  data["type"] = p_type;
  data["message"] = p_error;
  setData( "error", data );
}

#if QT_VERSION < 0x040500
DataEngine::Data FlickrEngine::config() const
{
  return m_config;
}

void FlickrEngine::setConfig( const DataEngine::Data &p_config )
{
  m_config = p_config;
#else
QVariant FlickrEngine::config() const
{
  return m_config;
}

void FlickrEngine::setConfig( const QVariant &p_config )
{
  m_config = p_config.toHash();
#endif
  m_size = m_config.value( "size" ).toString();
  m_token = m_config.value( "token" ).toString();
  setData( "authentication", "token", m_token );

  m_type = s_photoListTypes[ m_config.value( "listtype" ).toString() ];

  if ( m_config.value( "check_token" ).toBool() )
  {
    checkToken();
  }

  if ( m_config.value( "force_reload" ).toBool() )
  {
    retrievePhotos();
  }

  static int s_interval = 0;
  if ( s_interval != m_config.value( "interval" ).toInt() )
  {
    m_timer->stop();
    s_interval = m_config.value( "interval" ).toInt();
    m_timer->start( s_interval * 1000 );
  }
}

void FlickrEngine::markAsFavorite()
{
  Params p;
  p["method"] = "flickr.favorites.add";
  p["photo_id"] = ( *(m_photo_it - 1) ).id;
  p["auth_token"] = m_token;
  KIO::TransferJob *job = KIO::http_post( constructApiUrl( p, true ), QByteArray(), KIO::HideProgressInfo );
  job->addMetaData( "content-type", "Content-Type: application/x-www-form-urlencoded" );
}

void FlickrEngine::retrieveClusters( const QString &p_tag )
{
  Params p;
  p["method"] = "flickr.tags.getClusters";
  p["tag"] = p_tag;
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, false ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob * ) ), SLOT( clusterJobDone( KJob * ) ) );
}

void FlickrEngine::clusterJobDone( KJob *p_job )
{
  QDomDocument doc;
  if ( processResponse( p_job, &doc ) )
  {
    QDomNodeList errors = doc.elementsByTagName( "err" );
    if ( !errors.isEmpty() )
    {
      setError( i18n( "Could not retrieve clusters for given tag" ), ERR_MESSAGEBOX );
      return;
    }

    DataEngine::Data data;
    QDomNodeList clusters = doc.elementsByTagName( "cluster" );
    for ( uint i = 0; i < clusters.length(); ++i )
    {
      QDomNodeList tagElements = clusters.item( i ).childNodes();
      QStringList tags;
      for ( uint j = 0; j < tagElements.length(); ++j )
      {
        tags += tagElements.item( j ).toElement().text();
      }

      QString key = QString( "cluster%1" ).arg( i );
      data[ key ] = tags;
    }

    removeAllData( "clusters" );
    setData( "clusters", data );
  }
}

void FlickrEngine::retrievePhotosByCluster( const QString &p_tag, const QString &p_cluster_id )
{
  Params p;
  p["method"] = "flickr.tags.getClusterPhotos";
  p["tag"] = p_tag;
  p["cluster_id"] = p_cluster_id;
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, false ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob * ) ), SLOT( listJobDone( KJob * ) ) );
}

void FlickrEngine::retrieveNSID( const QString &p_username )
{
  Params p;
  p["method"] = "flickr.people.findByUsername";
  p["username"] = p_username;
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, false ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob * ) ), SLOT( nsidJobDone( KJob * ) ) );
}

void FlickrEngine::nsidJobDone( KJob *p_job )
{
  QDomDocument doc;
  if ( processResponse( p_job, &doc ) )
  {
    if ( !doc.elementsByTagName( "err" ).isEmpty() )
    {
      // error found
      setError( i18n( "User not found" ), ERR_MESSAGEBOX );
      return;
    }

    setData( "nsid", doc.elementsByTagName( "user" ).at( 0 ).toElement().attribute( "nsid" ) );
  }
}

void FlickrEngine::retrievePhotosets( const QString &p_nsid )
{
  Params p;
  p["method"] = "flickr.photosets.getList";
  p["auth_token"] = m_token;
  p["user_id"] = p_nsid.isEmpty() ? m_nsid : p_nsid;
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, true ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob * ) ), SLOT( photosetJobDone( KJob * ) ) );
}

void FlickrEngine::photosetJobDone( KJob *p_job )
{
  QDomDocument doc;
  if ( processResponse( p_job, &doc ) )
  {
    DataEngine::Data data;
    QDomNodeList photosets = doc.elementsByTagName( "photoset" );

    if ( photosets.isEmpty() )
    {
      setError( i18n( "No (public) photosets were found for this user. This could occur when the user has uploaded restricted content. Please follow these steps if you want to access restricted content:<ol><li>Create an account at Flickr if you don't have one already;</li><li>Disable SafeSearch. Go to <em>You</em>\342\200\243<em>Your&nbsp;Account</em>\342\200\243<em>Privacy&nbsp;&amp;&nbsp;Permissions</em>\342\200\243<em>Edit&nbsp;Search&nbsp;settings</em>\342\200\243<em>SafeSearch&nbsp;off</em></li><li>Authenticate Flickr On Plasma in the Authentication section.</li></ul>" ), ERR_MESSAGEBOX );
      return;
    }

    for ( uint i = 0; i < photosets.length(); ++i )
    {
      QDomElement photoset = photosets.item( i ).toElement();
      QString title = photoset.firstChildElement( "title" ).text();

      data[ photoset.attribute( "id" ) ] = title;
    }

    removeAllData( "photosets" );
    setData( "photosets", data );
  }
}

void FlickrEngine::retrievePhotoset( const QString &p_photoset_id )
{
  Params p;
  p["method"] = "flickr.photosets.getPhotos";
  p["auth_token"] = m_token;
  p["media"] = "photos";
  p["photoset_id"] = p_photoset_id;
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, true ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob * ) ), SLOT( listJobDone( KJob * ) ) );
}

void FlickrEngine::retrievePhotosByGeo( qreal p_longitude, qreal p_latitude, int p_accuracy )
{
  Params p;
  p["method"] = "flickr.photos.search";
  p["lon"] = QString::number( p_longitude );
  p["lat"] = QString::number( p_latitude );
  p["accuracy"] = QString::number( p_accuracy );
  KIO::StoredTransferJob *job = KIO::storedGet( constructApiUrl( p, true ), KIO::NoReload, KIO::HideProgressInfo );
  connect( job, SIGNAL( result( KJob * ) ), SLOT( listJobDone( KJob * ) ) );
}

K_EXPORT_PLASMA_DATAENGINE(flickrop, FlickrEngine)

