PageRenderTime 209ms CodeModel.GetById 81ms app.highlight 92ms RepoModel.GetById 29ms app.codeStats 0ms

/src/libtomahawk/resolvers/qtscriptresolver.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 644 lines | 465 code | 147 blank | 32 comment | 40 complexity | e848ed3233e07c3cadd4878d0e677d76 MD5 | raw file
  1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
  2 *
  3 *   Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
  4 *   Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
  5 *
  6 *   Tomahawk is free software: you can redistribute it and/or modify
  7 *   it under the terms of the GNU General Public License as published by
  8 *   the Free Software Foundation, either version 3 of the License, or
  9 *   (at your option) any later version.
 10 *
 11 *   Tomahawk is distributed in the hope that it will be useful,
 12 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 13 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 14 *   GNU General Public License for more details.
 15 *
 16 *   You should have received a copy of the GNU General Public License
 17 *   along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 18 */
 19
 20#include "QtScriptResolver.h"
 21
 22#include "Artist.h"
 23#include "Album.h"
 24#include "config.h"
 25#include "Pipeline.h"
 26#include "SourceList.h"
 27
 28#include "network/Servent.h"
 29
 30#include "utils/TomahawkUtils.h"
 31#include "utils/Logger.h"
 32
 33
 34#include <QtGui/QMessageBox>
 35
 36#include <QtNetwork/QNetworkRequest>
 37#include <QtNetwork/QNetworkReply>
 38
 39#include <QtCore/QMetaProperty>
 40#include <QtCore/QCryptographicHash>
 41
 42// FIXME: bloody hack, remove this for 0.3
 43// this one adds new functionality to old resolvers
 44#define RESOLVER_LEGACY_CODE "var resolver = Tomahawk.resolver.instance ? Tomahawk.resolver.instance : TomahawkResolver;"
 45// this one keeps old code invokable
 46#define RESOLVER_LEGACY_CODE2 "var resolver = Tomahawk.resolver.instance ? Tomahawk.resolver.instance : window;"
 47
 48
 49QtScriptResolverHelper::QtScriptResolverHelper( const QString& scriptPath, QtScriptResolver* parent )
 50    : QObject( parent )
 51{
 52    m_scriptPath = scriptPath;
 53    m_resolver = parent;
 54}
 55
 56
 57QByteArray
 58QtScriptResolverHelper::readRaw( const QString& fileName )
 59{
 60    QString path = QFileInfo( m_scriptPath ).absolutePath();
 61    // remove directories
 62    QString cleanedFileName = QFileInfo( fileName ).fileName();
 63    QString absoluteFilePath = path.append( "/" ).append( cleanedFileName );
 64
 65    QFile file( absoluteFilePath );
 66    if ( !file.exists() )
 67    {
 68        Q_ASSERT(false);
 69        return QByteArray();
 70    }
 71
 72    file.open( QIODevice::ReadOnly );
 73    return file.readAll();
 74}
 75
 76
 77QString
 78QtScriptResolverHelper::compress( const QString& data )
 79{
 80    QByteArray comp = qCompress( data.toLatin1(), 9 );
 81    return comp.toBase64();
 82}
 83
 84
 85QString
 86QtScriptResolverHelper::readCompressed( const QString& fileName )
 87{
 88    return compress( readRaw( fileName ) );
 89}
 90
 91
 92QString
 93QtScriptResolverHelper::readBase64( const QString& fileName )
 94{
 95    return readRaw( fileName ).toBase64();
 96}
 97
 98
 99QVariantMap
100QtScriptResolverHelper::resolverData()
101{
102    QVariantMap resolver;
103    resolver["config"] = m_resolverConfig;
104    resolver["scriptPath"] = m_scriptPath;
105    return resolver;
106}
107
108
109void
110QtScriptResolverHelper::log( const QString& message )
111{
112    tLog() << m_scriptPath << ":" << message;
113}
114
115
116void
117QtScriptResolverHelper::addTrackResults( const QVariantMap& results )
118{
119    qDebug() << "Resolver reporting results:" << results;
120    QList< Tomahawk::result_ptr > tracks = m_resolver->parseResultVariantList( results.value("results").toList() );
121
122    QString qid = results.value("qid").toString();
123
124    Tomahawk::Pipeline::instance()->reportResults( qid, tracks );
125}
126
127
128void
129QtScriptResolverHelper::setResolverConfig( const QVariantMap& config )
130{
131    m_resolverConfig = config;
132}
133
134
135QString
136QtScriptResolverHelper::hmac( const QByteArray& key, const QByteArray &input )
137{
138#ifdef QCA2_FOUND
139    if ( !QCA::isSupported( "hmac(md5)" ) )
140    {
141        tLog() << "HMAC(md5) not supported with qca-ossl plugin, or qca-ossl plugin is not installed! Unable to generate signature!";
142        return QByteArray();
143    }
144
145    QCA::MessageAuthenticationCode md5hmac1( "hmac(md5)", QCA::SecureArray() );
146    QCA::SymmetricKey keyObject( key );
147    md5hmac1.setup( keyObject );
148
149    md5hmac1.update( QCA::SecureArray( input ) );
150    QCA::SecureArray resultArray = md5hmac1.final();
151
152    QString result = QCA::arrayToHex( resultArray.toByteArray() );
153    return result.toUtf8();
154#else
155    tLog() << "Tomahawk compiled without QCA support, cannot generate HMAC signature";
156    return QString();
157#endif
158}
159
160
161QString
162QtScriptResolverHelper::md5( const QByteArray& input )
163{
164    QByteArray const digest = QCryptographicHash::hash( input, QCryptographicHash::Md5 );
165    return QString::fromLatin1( digest.toHex() );
166}
167
168
169void
170QtScriptResolverHelper::addCustomUrlHandler( const QString& protocol, const QString& callbackFuncName )
171{
172    boost::function<QSharedPointer<QIODevice>(Tomahawk::result_ptr)> fac = boost::bind( &QtScriptResolverHelper::customIODeviceFactory, this, _1 );
173    Servent::instance()->registerIODeviceFactory( protocol, fac );
174
175    m_urlCallback = callbackFuncName;
176}
177
178
179QByteArray
180QtScriptResolverHelper::base64Encode( const QByteArray& input )
181{
182    return input.toBase64();
183}
184
185
186QByteArray
187QtScriptResolverHelper::base64Decode( const QByteArray& input )
188{
189    return QByteArray::fromBase64( input );
190}
191
192
193QSharedPointer< QIODevice >
194QtScriptResolverHelper::customIODeviceFactory( const Tomahawk::result_ptr& result )
195{
196    QString getUrl = QString( "Tomahawk.resolver.instance.%1( '%2' );" ).arg( m_urlCallback )
197                                                                        .arg( QString( QUrl( result->url() ).toEncoded() ) );
198
199    QString urlStr = m_resolver->m_engine->mainFrame()->evaluateJavaScript( getUrl ).toString();
200
201    if ( urlStr.isEmpty() )
202        return QSharedPointer< QIODevice >();
203
204    QUrl url = QUrl::fromEncoded( urlStr.toUtf8() );
205    QNetworkRequest req( url );
206    tDebug() << "Creating a QNetowrkReply with url:" << req.url().toString();
207    QNetworkReply* reply = TomahawkUtils::nam()->get( req );
208    return QSharedPointer<QIODevice>( reply, &QObject::deleteLater );
209}
210
211
212void
213ScriptEngine::javaScriptConsoleMessage( const QString& message, int lineNumber, const QString& sourceID )
214{
215    tLog() << "JAVASCRIPT:" << m_scriptPath << message << lineNumber << sourceID;
216#ifndef QT_NO_DEBUG
217    QMessageBox::critical( 0, "Script Resolver Error", QString( "%1 %2 %3 %4" ).arg( m_scriptPath ).arg( message ).arg( lineNumber ).arg( sourceID ) );
218#endif
219}
220
221
222QtScriptResolver::QtScriptResolver( const QString& scriptPath )
223    : Tomahawk::ExternalResolverGui( scriptPath )
224    , m_ready( false )
225    , m_stopped( true )
226    , m_error( Tomahawk::ExternalResolver::NoError )
227    , m_resolverHelper( new QtScriptResolverHelper( scriptPath, this ) )
228{
229    tLog() << Q_FUNC_INFO << "Loading JS resolver:" << scriptPath;
230
231    m_engine = new ScriptEngine( this );
232    m_name = QFileInfo( filePath() ).baseName();
233
234    // set the icon, if we launch properly we'll get the icon the resolver reports
235    m_icon.load( RESPATH "images/resolver-default.png" );
236
237
238    if ( !QFile::exists( filePath() ) )
239    {
240        tLog() << Q_FUNC_INFO << "Failed loading JavaScript resolver:" << scriptPath;
241        m_error = Tomahawk::ExternalResolver::FileNotFound;
242    }
243    else
244    {
245        init();
246    }
247}
248
249
250QtScriptResolver::~QtScriptResolver()
251{
252    if ( !m_stopped )
253        stop();
254
255    delete m_engine;
256}
257
258
259Tomahawk::ExternalResolver* QtScriptResolver::factory( const QString& scriptPath )
260{
261    ExternalResolver* res = 0;
262
263    const QFileInfo fi( scriptPath );
264    if ( fi.suffix() == "js" || fi.suffix() == "script" )
265    {
266        res = new QtScriptResolver( scriptPath );
267        tLog() << Q_FUNC_INFO << scriptPath << "Loaded.";
268    }
269
270    return res;
271}
272
273
274bool
275QtScriptResolver::running() const
276{
277    return m_ready && !m_stopped;
278}
279
280
281void
282QtScriptResolver::reload()
283{
284    if ( QFile::exists( filePath() ) )
285    {
286        init();
287        m_error = Tomahawk::ExternalResolver::NoError;
288    } else
289    {
290        m_error = Tomahawk::ExternalResolver::FileNotFound;
291    }
292}
293
294
295void
296QtScriptResolver::init()
297{
298    QFile scriptFile( filePath() );
299    if( !scriptFile.open( QIODevice::ReadOnly ) )
300    {
301        qWarning() << "Failed to read contents of file:" << filePath() << scriptFile.errorString();
302        return;
303    }
304    const QByteArray scriptContents = scriptFile.readAll();
305
306    m_engine->mainFrame()->setHtml( "<html><body></body></html>", QUrl( "file:///invalid/file/for/security/policy" ) );
307
308    // add c++ part of tomahawk javascript library
309    m_engine->mainFrame()->addToJavaScriptWindowObject( "Tomahawk", m_resolverHelper );
310
311    // add rest of it
312    m_engine->setScriptPath( "tomahawk.js" );
313    QFile jslib( RESPATH "js/tomahawk.js" );
314    jslib.open( QIODevice::ReadOnly );
315    m_engine->mainFrame()->evaluateJavaScript( jslib.readAll() );
316    jslib.close();
317
318    // add resolver
319    m_engine->setScriptPath( filePath() );
320    m_engine->mainFrame()->evaluateJavaScript( scriptContents );
321
322    // init resolver
323    resolverInit();
324
325    QVariantMap m = resolverSettings();
326    m_name    = m.value( "name" ).toString();
327    m_weight  = m.value( "weight", 0 ).toUInt();
328    m_timeout = m.value( "timeout", 25 ).toUInt() * 1000;
329    QString iconPath = QFileInfo( filePath() ).path() + "/" + m.value( "icon" ).toString();
330    int success = m_icon.load( iconPath );
331
332    // load config widget and apply settings
333    loadUi();
334    QVariantMap config = resolverUserConfig();
335    fillDataInWidgets( config );
336
337    qDebug() << "JS" << filePath() << "READY," << "name" << m_name << "weight" << m_weight << "timeout" << m_timeout << "icon" << iconPath << "icon found" << success;
338
339    m_ready = true;
340}
341
342
343void
344QtScriptResolver::start()
345{
346    m_stopped = false;
347    if ( m_ready )
348        Tomahawk::Pipeline::instance()->addResolver( this );
349    else
350        init();
351}
352
353
354Tomahawk::ExternalResolver::ErrorState
355QtScriptResolver::error() const
356{
357    return m_error;
358}
359
360
361void
362QtScriptResolver::resolve( const Tomahawk::query_ptr& query )
363{
364    if ( QThread::currentThread() != thread() )
365    {
366        QMetaObject::invokeMethod( this, "resolve", Qt::QueuedConnection, Q_ARG(Tomahawk::query_ptr, query) );
367        return;
368    }
369
370    QString eval;
371    if ( !query->isFullTextQuery() )
372    {
373        eval = QString( RESOLVER_LEGACY_CODE2 "resolver.resolve( '%1', '%2', '%3', '%4' );" )
374                  .arg( query->id().replace( "'", "\\'" ) )
375                  .arg( query->artist().replace( "'", "\\'" ) )
376                  .arg( query->album().replace( "'", "\\'" ) )
377                  .arg( query->track().replace( "'", "\\'" ) );
378    }
379    else
380    {
381        eval = QString( "if(Tomahawk.resolver.instance !== undefined) {"
382                        "   resolver.search( '%1', '%2' );"
383                        "} else {"
384                        "   resolve( '%1', '', '', '%2' );"
385                        "}"
386                      )
387                  .arg( query->id().replace( "'", "\\'" ) )
388                  .arg( query->fullTextQuery().replace( "'", "\\'" ) );
389    }
390
391    QVariantMap m = m_engine->mainFrame()->evaluateJavaScript( eval ).toMap();
392    if ( m.isEmpty() )
393    {
394        // if the resolver doesn't return anything, async api is used
395        return;
396    }
397
398    qDebug() << "JavaScript Result:" << m;
399
400    const QString qid = query->id();
401    const QVariantList reslist = m.value( "results" ).toList();
402
403    QList< Tomahawk::result_ptr > results = parseResultVariantList( reslist );
404
405    Tomahawk::Pipeline::instance()->reportResults( qid, results );
406}
407
408
409QList< Tomahawk::result_ptr >
410QtScriptResolver::parseResultVariantList( const QVariantList& reslist )
411{
412    QList< Tomahawk::result_ptr > results;
413
414    foreach( const QVariant& rv, reslist )
415    {
416        QVariantMap m = rv.toMap();
417        if ( m.value( "artist" ).toString().trimmed().isEmpty() || m.value( "track" ).toString().trimmed().isEmpty() )
418            continue;
419
420        // TODO we need to handle preview urls separately. they should never trump a real url, and we need to display
421        // the purchaseUrl for the user to upgrade to a full stream.
422        if ( m.value( "preview" ).toBool() == true )
423            continue;
424
425        Tomahawk::result_ptr rp = Tomahawk::Result::get( m.value( "url" ).toString() );
426        Tomahawk::artist_ptr ap = Tomahawk::Artist::get( m.value( "artist" ).toString(), false );
427        rp->setArtist( ap );
428        rp->setAlbum( Tomahawk::Album::get( ap, m.value( "album" ).toString(), false ) );
429        rp->setTrack( m.value( "track" ).toString() );
430        rp->setAlbumPos( m.value( "albumpos" ).toUInt() );
431        rp->setBitrate( m.value( "bitrate" ).toUInt() );
432        rp->setSize( m.value( "size" ).toUInt() );
433        rp->setRID( uuid() );
434        rp->setFriendlySource( name() );
435        rp->setPurchaseUrl( m.value( "purchaseUrl" ).toString() );
436        rp->setLinkUrl( m.value( "linkUrl" ).toString() );
437        rp->setScore( m.value( "score" ).toFloat() );
438        rp->setDiscNumber( m.value( "discnumber" ).toUInt() );
439
440        if ( m.contains( "year" ) )
441        {
442            QVariantMap attr;
443            attr[ "releaseyear" ] = m.value( "year" );
444            rp->setAttributes( attr );
445        }
446
447        rp->setDuration( m.value( "duration", 0 ).toUInt() );
448        if ( rp->duration() <= 0 && m.contains( "durationString" ) )
449        {
450            QTime time = QTime::fromString( m.value( "durationString" ).toString(), "hh:mm:ss" );
451            rp->setDuration( time.secsTo( QTime( 0, 0 ) ) * -1 );
452        }
453
454        rp->setMimetype( m.value( "mimetype" ).toString() );
455        if ( rp->mimetype().isEmpty() )
456        {
457            rp->setMimetype( TomahawkUtils::extensionToMimetype( m.value( "extension" ).toString() ) );
458            Q_ASSERT( !rp->mimetype().isEmpty() );
459        }
460
461        rp->setResolvedBy( this );
462        results << rp;
463    }
464
465    return results;
466}
467
468
469void
470QtScriptResolver::stop()
471{
472    m_stopped = true;
473    Tomahawk::Pipeline::instance()->removeResolver( this );
474    emit stopped();
475}
476
477
478void
479QtScriptResolver::loadUi()
480{
481    QVariantMap m = m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE  "resolver.getConfigUi();" ).toMap();
482    m_dataWidgets = m["fields"].toList();
483
484    bool compressed = m.value( "compressed", "false" ).toBool();
485    qDebug() << "Resolver has a preferences widget! compressed?" << compressed;
486
487    QByteArray uiData = m[ "widget" ].toByteArray();
488
489    if( compressed )
490        uiData = qUncompress( QByteArray::fromBase64( uiData ) );
491    else
492        uiData = QByteArray::fromBase64( uiData );
493
494    QVariantMap images;
495    foreach(const QVariant& item, m[ "images" ].toList())
496    {
497        QString key = item.toMap().keys().first();
498        QVariant value = item.toMap().value(key);
499        images[key] = value;
500    }
501
502    if( m.contains( "images" ) )
503        uiData = fixDataImagePaths( uiData, compressed, images );
504
505    m_configWidget = QWeakPointer< QWidget >( widgetFromData( uiData, 0 ) );
506
507    emit changed();
508}
509
510
511QWidget*
512QtScriptResolver::configUI() const
513{
514    if( m_configWidget.isNull() )
515        return 0;
516    else
517        return m_configWidget.data();
518}
519
520
521void
522QtScriptResolver::saveConfig()
523{
524    QVariant saveData = loadDataFromWidgets();
525//    qDebug() << Q_FUNC_INFO << saveData;
526
527    m_resolverHelper->setResolverConfig( saveData.toMap() );
528    m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE "resolver.saveUserConfig();" );
529}
530
531
532QWidget*
533QtScriptResolver::findWidget(QWidget* widget, const QString& objectName)
534{
535    if( !widget || !widget->isWidgetType() )
536        return 0;
537
538    if( widget->objectName() == objectName )
539        return widget;
540
541
542    foreach( QObject* child, widget->children() )
543    {
544        QWidget* found = findWidget(qobject_cast< QWidget* >( child ), objectName);
545
546        if( found )
547            return found;
548    }
549
550    return 0;
551}
552
553
554QVariant
555QtScriptResolver::widgetData(QWidget* widget, const QString& property)
556{
557    for( int i = 0; i < widget->metaObject()->propertyCount(); i++ )
558    {
559        if( widget->metaObject()->property( i ).name() == property )
560        {
561            return widget->property( property.toLatin1() );
562        }
563    }
564
565    return QVariant();
566}
567
568
569void
570QtScriptResolver::setWidgetData(const QVariant& value, QWidget* widget, const QString& property)
571{
572    for( int i = 0; i < widget->metaObject()->propertyCount(); i++ )
573    {
574        if( widget->metaObject()->property( i ).name() == property )
575        {
576            widget->metaObject()->property( i ).write( widget, value);
577            return;
578        }
579    }
580}
581
582
583QVariantMap
584QtScriptResolver::loadDataFromWidgets()
585{
586    QVariantMap saveData;
587    foreach(const QVariant& dataWidget, m_dataWidgets)
588    {
589        QVariantMap data = dataWidget.toMap();
590
591        QString widgetName = data["widget"].toString();
592        QWidget* widget= findWidget( m_configWidget.data(), widgetName );
593
594        QVariant value = widgetData( widget, data["property"].toString() );
595
596        saveData[ data["name"].toString() ] = value;
597    }
598
599    return saveData;
600}
601
602
603void
604QtScriptResolver::fillDataInWidgets( const QVariantMap& data )
605{
606    foreach(const QVariant& dataWidget, m_dataWidgets)
607    {
608        QString widgetName = dataWidget.toMap()["widget"].toString();
609        QWidget* widget= findWidget( m_configWidget.data(), widgetName );
610        if( !widget )
611        {
612            tLog() << Q_FUNC_INFO << "Widget specified in resolver was not found:" << widgetName;
613            Q_ASSERT(false);
614            return;
615        }
616
617        QString propertyName = dataWidget.toMap()["property"].toString();
618        QString name = dataWidget.toMap()["name"].toString();
619
620        setWidgetData( data[ name ], widget, propertyName );
621    }
622}
623
624
625QVariantMap
626QtScriptResolver::resolverSettings()
627{
628    return m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE "if(resolver.settings) resolver.settings; else getSettings(); " ).toMap();
629}
630
631
632QVariantMap
633QtScriptResolver::resolverUserConfig()
634{
635    return m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE "resolver.getUserConfig();" ).toMap();
636}
637
638
639QVariantMap
640QtScriptResolver::resolverInit()
641{
642    return m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE "resolver.init();" ).toMap();
643}
644