PageRenderTime 196ms CodeModel.GetById 50ms app.highlight 88ms RepoModel.GetById 43ms app.codeStats 0ms

/src/libtomahawk/resolvers/ScriptResolver.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 588 lines | 439 code | 114 blank | 35 comment | 60 complexity | 29c676497c2d5a938068aa99f16aa91e 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 *   Copyright 2013,      Teo Mrnjavac           <teo@kde.org>
  6 *
  7 *   Tomahawk is free software: you can redistribute it and/or modify
  8 *   it under the terms of the GNU General Public License as published by
  9 *   the Free Software Foundation, either version 3 of the License, or
 10 *   (at your option) any later version.
 11 *
 12 *   Tomahawk is distributed in the hope that it will be useful,
 13 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 14 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 15 *   GNU General Public License for more details.
 16 *
 17 *   You should have received a copy of the GNU General Public License
 18 *   along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 19 */
 20
 21#include "ScriptResolver.h"
 22
 23#include "accounts/AccountConfigWidget.h"
 24#include "utils/TomahawkUtilsGui.h"
 25#include "utils/Json.h"
 26#include "utils/Logger.h"
 27#include "utils/NetworkAccessManager.h"
 28#include "utils/NetworkProxyFactory.h"
 29
 30#include "Artist.h"
 31#include "Album.h"
 32#include "Pipeline.h"
 33#include "Result.h"
 34#include "ScriptCollection.h"
 35#include "SourceList.h"
 36#include "Track.h"
 37
 38#include <QtEndian>
 39#include <QFileInfo>
 40#include <QNetworkAccessManager>
 41#include <QNetworkProxy>
 42
 43#ifdef Q_OS_WIN
 44#include <shlwapi.h>
 45#endif
 46
 47using namespace Tomahawk;
 48
 49ScriptResolver::ScriptResolver( const QString& exe )
 50    : Tomahawk::ExternalResolverGui( exe )
 51    , m_num_restarts( 0 )
 52    , m_msgsize( 0 )
 53    , m_ready( false )
 54    , m_stopped( true )
 55    , m_configSent( false )
 56    , m_deleting( false )
 57    , m_error( Tomahawk::ExternalResolver::NoError )
 58{
 59    tLog() << Q_FUNC_INFO << "Created script resolver:" << exe;
 60    connect( &m_proc, SIGNAL( readyReadStandardError() ), SLOT( readStderr() ) );
 61    connect( &m_proc, SIGNAL( readyReadStandardOutput() ), SLOT( readStdout() ) );
 62    connect( &m_proc, SIGNAL( finished( int, QProcess::ExitStatus ) ), SLOT( cmdExited( int, QProcess::ExitStatus ) ) );
 63
 64    startProcess();
 65
 66    if ( !Tomahawk::Utils::nam() )
 67        return;
 68
 69    // set the name to the binary, if we launch properly we'll get the name the resolver reports
 70    m_name = QFileInfo( filePath() ).baseName();
 71
 72    // set the icon, if we launch properly we'll get the icon the resolver reports
 73    m_icon = TomahawkUtils::defaultPixmap( TomahawkUtils::DefaultResolver, TomahawkUtils::Original, QSize( 128, 128 ) );
 74}
 75
 76
 77ScriptResolver::~ScriptResolver()
 78{
 79    disconnect( &m_proc, SIGNAL( finished( int, QProcess::ExitStatus ) ), this, SLOT( cmdExited( int, QProcess::ExitStatus ) ) );
 80    m_deleting = true;
 81
 82    QVariantMap msg;
 83    msg[ "_msgtype" ] = "quit";
 84    sendMessage( msg );
 85
 86    bool finished = m_proc.state() != QProcess::Running || m_proc.waitForFinished( 2500 ); // might call handleMsg
 87
 88    Tomahawk::Pipeline::instance()->removeResolver( this );
 89
 90    if ( !finished || m_proc.state() == QProcess::Running )
 91    {
 92        qDebug() << "External resolver didn't exit after waiting 2s for it to die, killing forcefully";
 93#ifdef Q_OS_WIN
 94        m_proc.kill();
 95#else
 96        m_proc.terminate();
 97#endif
 98    }
 99
100    if ( !m_configWidget.isNull() )
101        delete m_configWidget.data();
102}
103
104
105Tomahawk::ExternalResolver*
106ScriptResolver::factory( const QString& accountId, const QString& exe, const QStringList& unused )
107{
108    Q_UNUSED( accountId )
109    Q_UNUSED( unused )
110
111    ExternalResolver* res = 0;
112
113    const QFileInfo fi( exe );
114    if ( fi.suffix() != "js" && fi.suffix() != "script" )
115    {
116        res = new ScriptResolver( exe );
117        tLog() << Q_FUNC_INFO << exe << "Loaded.";
118    }
119
120    return res;
121}
122
123
124void
125ScriptResolver::start()
126{
127    m_stopped = false;
128    if ( m_ready )
129        Tomahawk::Pipeline::instance()->addResolver( this );
130    else if ( !m_configSent )
131        sendConfig();
132    // else, we've sent our config msg so are waiting for the resolver to react
133}
134
135
136void
137ScriptResolver::sendConfig()
138{
139    // Send a configutaion message with any information the resolver might need
140    // For now, only the proxy information is sent
141    QVariantMap m;
142    m.insert( "_msgtype", "config" );
143
144    m_configSent = true;
145
146    tDebug() << "Nam is:" << Tomahawk::Utils::nam();
147    tDebug() << "Nam proxy is:" << Tomahawk::Utils::nam()->proxyFactory();
148    Tomahawk::Utils::nam()->proxyFactory()->queryProxy();
149    Tomahawk::Utils::NetworkProxyFactory* factory = dynamic_cast<Tomahawk::Utils::NetworkProxyFactory*>( Tomahawk::Utils::nam()->proxyFactory() );
150    QNetworkProxy proxy = factory->proxy();
151    QString proxyType = ( proxy.type() == QNetworkProxy::Socks5Proxy ? "socks5" : "none" );
152    m.insert( "proxytype", proxyType );
153    m.insert( "proxyhost", proxy.hostName() );
154    m.insert( "proxyport", proxy.port() );
155    m.insert( "proxyuser", proxy.user() );
156    m.insert( "proxypass", proxy.password() );
157
158    // QJson sucks
159    QVariantList hosts;
160    foreach ( const QString& host, factory->noProxyHosts() )
161        hosts << host;
162    m.insert( "noproxyhosts", hosts );
163
164    bool ok;
165    QByteArray data = TomahawkUtils::toJson( m, &ok );
166    Q_ASSERT( ok );
167    sendMsg( data );
168}
169
170
171void
172ScriptResolver::reload()
173{
174    startProcess();
175}
176
177
178bool
179ScriptResolver::running() const
180{
181    return !m_stopped;
182}
183
184
185void
186ScriptResolver::sendMessage( const QVariantMap& map )
187{
188    bool ok;
189    QByteArray data = TomahawkUtils::toJson( map, &ok );
190    Q_ASSERT( ok );
191    sendMsg( data );
192}
193
194
195void
196ScriptResolver::readStderr()
197{
198    tLog() << "SCRIPT_STDERR" << filePath() << m_proc.readAllStandardError();
199}
200
201
202ScriptResolver::ErrorState
203ScriptResolver::error() const
204{
205    return m_error;
206}
207
208
209void
210ScriptResolver::readStdout()
211{
212    if ( m_msgsize == 0 )
213    {
214        if ( m_proc.bytesAvailable() < 4 )
215            return;
216
217        quint32 len_nbo;
218        m_proc.read( (char*) &len_nbo, 4 );
219        m_msgsize = qFromBigEndian( len_nbo );
220    }
221
222    if ( m_msgsize > 0 )
223    {
224        m_msg.append( m_proc.read( m_msgsize - m_msg.length() ) );
225    }
226
227    if ( m_msgsize == (quint32) m_msg.length() )
228    {
229        handleMsg( m_msg );
230        m_msgsize = 0;
231        m_msg.clear();
232
233        if ( m_proc.bytesAvailable() )
234            QTimer::singleShot( 0, this, SLOT( readStdout() ) );
235    }
236}
237
238
239void
240ScriptResolver::sendMsg( const QByteArray& msg )
241{
242//     qDebug() << Q_FUNC_INFO << m_ready << msg << msg.length();
243    if ( !m_proc.isOpen() )
244        return;
245
246    quint32 len;
247    qToBigEndian( msg.length(), (uchar*) &len );
248    m_proc.write( (const char*) &len, 4 );
249    m_proc.write( msg );
250}
251
252
253void
254ScriptResolver::handleMsg( const QByteArray& msg )
255{
256//    qDebug() << Q_FUNC_INFO << msg.size() << QString::fromAscii( msg );
257
258    // Might be called from waitForFinished() in ~ScriptResolver, no database in that case, abort.
259    if ( m_deleting )
260        return;
261
262    bool ok;
263    QVariant v = TomahawkUtils::parseJson( msg, &ok );
264    if ( !ok || v.type() != QVariant::Map )
265    {
266        Q_ASSERT( false );
267        return;
268    }
269    QVariantMap m = v.toMap();
270    QString msgtype = m.value( "_msgtype" ).toString();
271
272    if ( msgtype == "settings" )
273    {
274        doSetup( m );
275        return;
276    }
277    else if ( msgtype == "confwidget" )
278    {
279        setupConfWidget( m );
280        return;
281    }
282    else if ( msgtype == "results" )
283    {
284        const QString qid = m.value( "qid" ).toString();
285        QList< Tomahawk::result_ptr > results;
286        const QVariantList reslist = m.value( "results" ).toList();
287
288        foreach( const QVariant& rv, reslist )
289        {
290            QVariantMap m = rv.toMap();
291            tDebug( LOGVERBOSE ) << "Found result:" << m;
292
293            Tomahawk::track_ptr track = Tomahawk::Track::get( m.value( "artist" ).toString(),
294                                                              m.value( "track" ).toString(),
295                                                              m.value( "album" ).toString(),
296                                                              m.value( "albumartist" ).toString(),
297                                                              m.value( "duration" ).toUInt(),
298                                                              QString(),
299                                                              m.value( "albumpos" ).toUInt(),
300                                                              m.value( "discnumber" ).toUInt() );
301            if ( !track )
302                continue;
303
304            Tomahawk::result_ptr rp = Tomahawk::Result::get( m.value( "url" ).toString(), track );
305            if ( !rp )
306                continue;
307
308            rp->setBitrate( m.value( "bitrate" ).toUInt() );
309            rp->setSize( m.value( "size" ).toUInt() );
310            rp->setRID( uuid() );
311            rp->setFriendlySource( m_name );
312            rp->setPurchaseUrl( m.value( "purchaseUrl" ).toString() );
313            rp->setLinkUrl( m.value( "linkUrl" ).toString() );
314
315            //FIXME
316            if ( m.contains( "year" ) )
317            {
318                QVariantMap attr;
319                attr[ "releaseyear" ] = m.value( "year" );
320//                rp->track()->setAttributes( attr );
321            }
322
323            rp->setMimetype( m.value( "mimetype" ).toString() );
324            if ( rp->mimetype().isEmpty() )
325            {
326                rp->setMimetype( TomahawkUtils::extensionToMimetype( m.value( "extension" ).toString() ) );
327                Q_ASSERT( !rp->mimetype().isEmpty() );
328            }
329
330            rp->setResolvedByResolver( this );
331            results << rp;
332        }
333
334        Tomahawk::Pipeline::instance()->reportResults( qid, this, results );
335    }
336    else
337    {
338        // Unknown message, give up for custom implementations
339        emit customMessage( msgtype, m );
340    }
341}
342
343
344void
345ScriptResolver::cmdExited( int code, QProcess::ExitStatus status )
346{
347    m_ready = false;
348    tLog() << Q_FUNC_INFO << "SCRIPT EXITED, code" << code << "status" << status << filePath();
349    Tomahawk::Pipeline::instance()->removeResolver( this );
350
351    m_error = ExternalResolver::FailedToLoad;
352    emit changed();
353
354    if ( m_stopped )
355    {
356        tLog() << "*** Script resolver stopped ";
357        emit terminated();
358
359        return;
360    }
361
362    if ( m_num_restarts < 10 )
363    {
364        m_num_restarts++;
365        tLog() << "*** Restart num" << m_num_restarts;
366        startProcess();
367        sendConfig();
368    }
369    else
370    {
371        tLog() << "*** Reached max restarts, not restarting.";
372    }
373}
374
375
376void
377ScriptResolver::resolve( const Tomahawk::query_ptr& query )
378{
379    QVariantMap m;
380    m.insert( "_msgtype", "rq" );
381
382    if ( query->isFullTextQuery() )
383    {
384        m.insert( "fulltext", query->fullTextQuery() );
385        m.insert( "track", query->fullTextQuery() );
386        m.insert( "qid", query->id() );
387    }
388    else
389    {
390        m.insert( "artist", query->queryTrack()->artist() );
391        m.insert( "track", query->queryTrack()->track() );
392        m.insert( "qid", query->id() );
393
394        if ( !query->resultHint().isEmpty() )
395            m.insert( "resultHint", query->resultHint() );
396    }
397
398    const QByteArray msg = TomahawkUtils::toJson( QVariant( m ) );
399    sendMsg( msg );
400}
401
402
403void
404ScriptResolver::doSetup( const QVariantMap& m )
405{
406//    qDebug() << Q_FUNC_INFO << m;
407
408    m_name    = m.value( "name" ).toString();
409    m_weight  = m.value( "weight", 0 ).toUInt();
410    m_timeout = m.value( "timeout", 5 ).toUInt() * 1000;
411    bool compressed = m.value( "compressed", "false" ).toString() == "true";
412
413    bool ok;
414    int intCap = m.value( "capabilities" ).toInt( &ok );
415    if ( !ok )
416        m_capabilities = NullCapability;
417    else
418        m_capabilities = static_cast< Capabilities >( intCap );
419
420    QByteArray icoData = m.value( "icon" ).toByteArray();
421    if ( compressed )
422        icoData = qUncompress( QByteArray::fromBase64( icoData ) );
423    else
424        icoData = QByteArray::fromBase64( icoData );
425    QPixmap ico;
426    ico.loadFromData( icoData );
427
428    bool success = false;
429    if ( !ico.isNull() )
430    {
431        m_icon = ico.scaled( m_icon.size(), Qt::IgnoreAspectRatio );
432        success = true;
433    }
434    // see if the resolver sent an icon path to not break the old (unofficial) api.
435    // TODO: remove this and publish a definitive api
436    if ( !success )
437    {
438        const QString iconPath = QFileInfo( filePath() ).path() + "/" + m.value( "icon" ).toString();
439        QPixmap icon;
440        icon.load( iconPath );
441        success = icon.load( iconPath );
442        if ( success )
443            m_icon = icon;
444    }
445
446    qDebug() << "SCRIPT" << filePath() << "READY," << "name" << m_name << "weight" << m_weight << "timeout" << m_timeout << "icon received" << success;
447
448    m_ready = true;
449    m_configSent = false;
450    m_num_restarts = 0;
451
452    if ( !m_stopped )
453        Tomahawk::Pipeline::instance()->addResolver( this );
454
455    emit changed();
456}
457
458
459void
460ScriptResolver::setupConfWidget( const QVariantMap& m )
461{
462    bool compressed = m.value( "compressed", "false" ).toString() == "true";
463    qDebug() << "Resolver has a preferences widget! compressed?" << compressed;
464
465    QByteArray uiData = m[ "widget" ].toByteArray();
466    if( compressed )
467        uiData = qUncompress( QByteArray::fromBase64( uiData ) );
468    else
469        uiData = QByteArray::fromBase64( uiData );
470
471    if ( m.contains( "images" ) )
472        uiData = fixDataImagePaths( uiData, compressed, m[ "images" ].toMap() );
473    m_configWidget = QPointer< AccountConfigWidget >( widgetFromData( uiData, 0 ) );
474
475    emit changed();
476}
477
478
479void
480ScriptResolver::startProcess()
481{
482    if ( !QFile::exists( filePath() ) )
483        m_error = Tomahawk::ExternalResolver::FileNotFound;
484    else
485    {
486        m_error = Tomahawk::ExternalResolver::NoError;
487    }
488
489    const QFileInfo fi( filePath() );
490
491    QString interpreter;
492    // have to enclose in quotes if path contains spaces...
493    const QString runPath = QString( "\"%1\"" ).arg( filePath() );
494
495    QFile file( filePath() );
496    file.setPermissions( file.permissions() | QFile::ExeOwner | QFile::ExeGroup | QFile::ExeOther );
497
498#ifdef Q_OS_WIN
499    if ( fi.suffix().toLower() != "exe" )
500    {
501        DWORD dwSize = MAX_PATH;
502
503        wchar_t path[MAX_PATH] = { 0 };
504        wchar_t *ext = (wchar_t *) ("." + fi.suffix()).utf16();
505
506        HRESULT hr = AssocQueryStringW(
507                (ASSOCF) 0,
508                ASSOCSTR_EXECUTABLE,
509                ext,
510                L"open",
511                path,
512                &dwSize
513        );
514
515        if ( ! FAILED( hr ) )
516        {
517            interpreter = QString( "\"%1\"" ).arg(QString::fromUtf16((const ushort *) path));
518        }
519    }
520#endif // Q_OS_WIN
521
522    if ( interpreter.isEmpty() )
523    {
524#ifndef Q_OS_WIN
525        const QFileInfo info( filePath() );
526        m_proc.setWorkingDirectory( info.absolutePath() );
527        tLog() << "Setting working dir:" << info.absolutePath();
528#endif
529        m_proc.start( runPath );
530    }
531    else
532        m_proc.start( interpreter, QStringList() << filePath() );
533
534    sendConfig();
535}
536
537
538void
539ScriptResolver::saveConfig()
540{
541    Q_ASSERT( !m_configWidget.isNull() );
542
543    QVariantMap m;
544    m.insert( "_msgtype", "setpref" );
545    QVariant widgets = configMsgFromWidget( m_configWidget.data() );
546    m.insert( "widgets", widgets );
547    bool ok;
548    QByteArray data = TomahawkUtils::toJson( m, &ok );
549    Q_ASSERT( ok );
550
551    sendMsg( data );
552}
553
554
555void
556ScriptResolver::setIcon( const QPixmap& icon )
557{
558    m_icon = icon;
559}
560
561
562QPixmap
563ScriptResolver::icon( const QSize& size ) const
564{
565    if ( !size.isEmpty() )
566        return m_icon.scaled( size, Qt::KeepAspectRatio, Qt::SmoothTransformation );
567
568    return m_icon;
569}
570
571
572AccountConfigWidget*
573ScriptResolver::configUI() const
574{
575    if ( m_configWidget.isNull() )
576        return 0;
577    else
578        return m_configWidget.data();
579}
580
581
582void
583ScriptResolver::stop()
584{
585    m_stopped = true;
586
587    Tomahawk::Pipeline::instance()->removeResolver( this );
588}