~ubuntu-branches/ubuntu/trusty/tomahawk/trusty-proposed

« back to all changes in this revision

Viewing changes to src/libtomahawk/resolvers/ScriptResolver.cpp

  • Committer: Package Import Robot
  • Author(s): Harald Sitter
  • Date: 2013-03-07 21:50:13 UTC
  • Revision ID: package-import@ubuntu.com-20130307215013-6gdjkdds7i9uenvs
Tags: upstream-0.6.0+dfsg
ImportĀ upstreamĀ versionĀ 0.6.0+dfsg

Show diffs side-by-side

added added

removed removed

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