/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. #include "QtScriptResolver.h"
  20. #include "Artist.h"
  21. #include "Album.h"
  22. #include "config.h"
  23. #include "Pipeline.h"
  24. #include "SourceList.h"
  25. #include "network/Servent.h"
  26. #include "utils/TomahawkUtils.h"
  27. #include "utils/Logger.h"
  28. #include <QtGui/QMessageBox>
  29. #include <QtNetwork/QNetworkRequest>
  30. #include <QtNetwork/QNetworkReply>
  31. #include <QtCore/QMetaProperty>
  32. #include <QtCore/QCryptographicHash>
  33. // FIXME: bloody hack, remove this for 0.3
  34. // this one adds new functionality to old resolvers
  35. #define RESOLVER_LEGACY_CODE "var resolver = Tomahawk.resolver.instance ? Tomahawk.resolver.instance : TomahawkResolver;"
  36. // this one keeps old code invokable
  37. #define RESOLVER_LEGACY_CODE2 "var resolver = Tomahawk.resolver.instance ? Tomahawk.resolver.instance : window;"
  38. QtScriptResolverHelper::QtScriptResolverHelper( const QString& scriptPath, QtScriptResolver* parent )
  39. : QObject( parent )
  40. {
  41. m_scriptPath = scriptPath;
  42. m_resolver = parent;
  43. }
  44. QByteArray
  45. QtScriptResolverHelper::readRaw( const QString& fileName )
  46. {
  47. QString path = QFileInfo( m_scriptPath ).absolutePath();
  48. // remove directories
  49. QString cleanedFileName = QFileInfo( fileName ).fileName();
  50. QString absoluteFilePath = path.append( "/" ).append( cleanedFileName );
  51. QFile file( absoluteFilePath );
  52. if ( !file.exists() )
  53. {
  54. Q_ASSERT(false);
  55. return QByteArray();
  56. }
  57. file.open( QIODevice::ReadOnly );
  58. return file.readAll();
  59. }
  60. QString
  61. QtScriptResolverHelper::compress( const QString& data )
  62. {
  63. QByteArray comp = qCompress( data.toLatin1(), 9 );
  64. return comp.toBase64();
  65. }
  66. QString
  67. QtScriptResolverHelper::readCompressed( const QString& fileName )
  68. {
  69. return compress( readRaw( fileName ) );
  70. }
  71. QString
  72. QtScriptResolverHelper::readBase64( const QString& fileName )
  73. {
  74. return readRaw( fileName ).toBase64();
  75. }
  76. QVariantMap
  77. QtScriptResolverHelper::resolverData()
  78. {
  79. QVariantMap resolver;
  80. resolver["config"] = m_resolverConfig;
  81. resolver["scriptPath"] = m_scriptPath;
  82. return resolver;
  83. }
  84. void
  85. QtScriptResolverHelper::log( const QString& message )
  86. {
  87. tLog() << m_scriptPath << ":" << message;
  88. }
  89. void
  90. QtScriptResolverHelper::addTrackResults( const QVariantMap& results )
  91. {
  92. qDebug() << "Resolver reporting results:" << results;
  93. QList< Tomahawk::result_ptr > tracks = m_resolver->parseResultVariantList( results.value("results").toList() );
  94. QString qid = results.value("qid").toString();
  95. Tomahawk::Pipeline::instance()->reportResults( qid, tracks );
  96. }
  97. void
  98. QtScriptResolverHelper::setResolverConfig( const QVariantMap& config )
  99. {
  100. m_resolverConfig = config;
  101. }
  102. QString
  103. QtScriptResolverHelper::hmac( const QByteArray& key, const QByteArray &input )
  104. {
  105. #ifdef QCA2_FOUND
  106. if ( !QCA::isSupported( "hmac(md5)" ) )
  107. {
  108. tLog() << "HMAC(md5) not supported with qca-ossl plugin, or qca-ossl plugin is not installed! Unable to generate signature!";
  109. return QByteArray();
  110. }
  111. QCA::MessageAuthenticationCode md5hmac1( "hmac(md5)", QCA::SecureArray() );
  112. QCA::SymmetricKey keyObject( key );
  113. md5hmac1.setup( keyObject );
  114. md5hmac1.update( QCA::SecureArray( input ) );
  115. QCA::SecureArray resultArray = md5hmac1.final();
  116. QString result = QCA::arrayToHex( resultArray.toByteArray() );
  117. return result.toUtf8();
  118. #else
  119. tLog() << "Tomahawk compiled without QCA support, cannot generate HMAC signature";
  120. return QString();
  121. #endif
  122. }
  123. QString
  124. QtScriptResolverHelper::md5( const QByteArray& input )
  125. {
  126. QByteArray const digest = QCryptographicHash::hash( input, QCryptographicHash::Md5 );
  127. return QString::fromLatin1( digest.toHex() );
  128. }
  129. void
  130. QtScriptResolverHelper::addCustomUrlHandler( const QString& protocol, const QString& callbackFuncName )
  131. {
  132. boost::function<QSharedPointer<QIODevice>(Tomahawk::result_ptr)> fac = boost::bind( &QtScriptResolverHelper::customIODeviceFactory, this, _1 );
  133. Servent::instance()->registerIODeviceFactory( protocol, fac );
  134. m_urlCallback = callbackFuncName;
  135. }
  136. QByteArray
  137. QtScriptResolverHelper::base64Encode( const QByteArray& input )
  138. {
  139. return input.toBase64();
  140. }
  141. QByteArray
  142. QtScriptResolverHelper::base64Decode( const QByteArray& input )
  143. {
  144. return QByteArray::fromBase64( input );
  145. }
  146. QSharedPointer< QIODevice >
  147. QtScriptResolverHelper::customIODeviceFactory( const Tomahawk::result_ptr& result )
  148. {
  149. QString getUrl = QString( "Tomahawk.resolver.instance.%1( '%2' );" ).arg( m_urlCallback )
  150. .arg( QString( QUrl( result->url() ).toEncoded() ) );
  151. QString urlStr = m_resolver->m_engine->mainFrame()->evaluateJavaScript( getUrl ).toString();
  152. if ( urlStr.isEmpty() )
  153. return QSharedPointer< QIODevice >();
  154. QUrl url = QUrl::fromEncoded( urlStr.toUtf8() );
  155. QNetworkRequest req( url );
  156. tDebug() << "Creating a QNetowrkReply with url:" << req.url().toString();
  157. QNetworkReply* reply = TomahawkUtils::nam()->get( req );
  158. return QSharedPointer<QIODevice>( reply, &QObject::deleteLater );
  159. }
  160. void
  161. ScriptEngine::javaScriptConsoleMessage( const QString& message, int lineNumber, const QString& sourceID )
  162. {
  163. tLog() << "JAVASCRIPT:" << m_scriptPath << message << lineNumber << sourceID;
  164. #ifndef QT_NO_DEBUG
  165. QMessageBox::critical( 0, "Script Resolver Error", QString( "%1 %2 %3 %4" ).arg( m_scriptPath ).arg( message ).arg( lineNumber ).arg( sourceID ) );
  166. #endif
  167. }
  168. QtScriptResolver::QtScriptResolver( const QString& scriptPath )
  169. : Tomahawk::ExternalResolverGui( scriptPath )
  170. , m_ready( false )
  171. , m_stopped( true )
  172. , m_error( Tomahawk::ExternalResolver::NoError )
  173. , m_resolverHelper( new QtScriptResolverHelper( scriptPath, this ) )
  174. {
  175. tLog() << Q_FUNC_INFO << "Loading JS resolver:" << scriptPath;
  176. m_engine = new ScriptEngine( this );
  177. m_name = QFileInfo( filePath() ).baseName();
  178. // set the icon, if we launch properly we'll get the icon the resolver reports
  179. m_icon.load( RESPATH "images/resolver-default.png" );
  180. if ( !QFile::exists( filePath() ) )
  181. {
  182. tLog() << Q_FUNC_INFO << "Failed loading JavaScript resolver:" << scriptPath;
  183. m_error = Tomahawk::ExternalResolver::FileNotFound;
  184. }
  185. else
  186. {
  187. init();
  188. }
  189. }
  190. QtScriptResolver::~QtScriptResolver()
  191. {
  192. if ( !m_stopped )
  193. stop();
  194. delete m_engine;
  195. }
  196. Tomahawk::ExternalResolver* QtScriptResolver::factory( const QString& scriptPath )
  197. {
  198. ExternalResolver* res = 0;
  199. const QFileInfo fi( scriptPath );
  200. if ( fi.suffix() == "js" || fi.suffix() == "script" )
  201. {
  202. res = new QtScriptResolver( scriptPath );
  203. tLog() << Q_FUNC_INFO << scriptPath << "Loaded.";
  204. }
  205. return res;
  206. }
  207. bool
  208. QtScriptResolver::running() const
  209. {
  210. return m_ready && !m_stopped;
  211. }
  212. void
  213. QtScriptResolver::reload()
  214. {
  215. if ( QFile::exists( filePath() ) )
  216. {
  217. init();
  218. m_error = Tomahawk::ExternalResolver::NoError;
  219. } else
  220. {
  221. m_error = Tomahawk::ExternalResolver::FileNotFound;
  222. }
  223. }
  224. void
  225. QtScriptResolver::init()
  226. {
  227. QFile scriptFile( filePath() );
  228. if( !scriptFile.open( QIODevice::ReadOnly ) )
  229. {
  230. qWarning() << "Failed to read contents of file:" << filePath() << scriptFile.errorString();
  231. return;
  232. }
  233. const QByteArray scriptContents = scriptFile.readAll();
  234. m_engine->mainFrame()->setHtml( "<html><body></body></html>", QUrl( "file:///invalid/file/for/security/policy" ) );
  235. // add c++ part of tomahawk javascript library
  236. m_engine->mainFrame()->addToJavaScriptWindowObject( "Tomahawk", m_resolverHelper );
  237. // add rest of it
  238. m_engine->setScriptPath( "tomahawk.js" );
  239. QFile jslib( RESPATH "js/tomahawk.js" );
  240. jslib.open( QIODevice::ReadOnly );
  241. m_engine->mainFrame()->evaluateJavaScript( jslib.readAll() );
  242. jslib.close();
  243. // add resolver
  244. m_engine->setScriptPath( filePath() );
  245. m_engine->mainFrame()->evaluateJavaScript( scriptContents );
  246. // init resolver
  247. resolverInit();
  248. QVariantMap m = resolverSettings();
  249. m_name = m.value( "name" ).toString();
  250. m_weight = m.value( "weight", 0 ).toUInt();
  251. m_timeout = m.value( "timeout", 25 ).toUInt() * 1000;
  252. QString iconPath = QFileInfo( filePath() ).path() + "/" + m.value( "icon" ).toString();
  253. int success = m_icon.load( iconPath );
  254. // load config widget and apply settings
  255. loadUi();
  256. QVariantMap config = resolverUserConfig();
  257. fillDataInWidgets( config );
  258. qDebug() << "JS" << filePath() << "READY," << "name" << m_name << "weight" << m_weight << "timeout" << m_timeout << "icon" << iconPath << "icon found" << success;
  259. m_ready = true;
  260. }
  261. void
  262. QtScriptResolver::start()
  263. {
  264. m_stopped = false;
  265. if ( m_ready )
  266. Tomahawk::Pipeline::instance()->addResolver( this );
  267. else
  268. init();
  269. }
  270. Tomahawk::ExternalResolver::ErrorState
  271. QtScriptResolver::error() const
  272. {
  273. return m_error;
  274. }
  275. void
  276. QtScriptResolver::resolve( const Tomahawk::query_ptr& query )
  277. {
  278. if ( QThread::currentThread() != thread() )
  279. {
  280. QMetaObject::invokeMethod( this, "resolve", Qt::QueuedConnection, Q_ARG(Tomahawk::query_ptr, query) );
  281. return;
  282. }
  283. QString eval;
  284. if ( !query->isFullTextQuery() )
  285. {
  286. eval = QString( RESOLVER_LEGACY_CODE2 "resolver.resolve( '%1', '%2', '%3', '%4' );" )
  287. .arg( query->id().replace( "'", "\\'" ) )
  288. .arg( query->artist().replace( "'", "\\'" ) )
  289. .arg( query->album().replace( "'", "\\'" ) )
  290. .arg( query->track().replace( "'", "\\'" ) );
  291. }
  292. else
  293. {
  294. eval = QString( "if(Tomahawk.resolver.instance !== undefined) {"
  295. " resolver.search( '%1', '%2' );"
  296. "} else {"
  297. " resolve( '%1', '', '', '%2' );"
  298. "}"
  299. )
  300. .arg( query->id().replace( "'", "\\'" ) )
  301. .arg( query->fullTextQuery().replace( "'", "\\'" ) );
  302. }
  303. QVariantMap m = m_engine->mainFrame()->evaluateJavaScript( eval ).toMap();
  304. if ( m.isEmpty() )
  305. {
  306. // if the resolver doesn't return anything, async api is used
  307. return;
  308. }
  309. qDebug() << "JavaScript Result:" << m;
  310. const QString qid = query->id();
  311. const QVariantList reslist = m.value( "results" ).toList();
  312. QList< Tomahawk::result_ptr > results = parseResultVariantList( reslist );
  313. Tomahawk::Pipeline::instance()->reportResults( qid, results );
  314. }
  315. QList< Tomahawk::result_ptr >
  316. QtScriptResolver::parseResultVariantList( const QVariantList& reslist )
  317. {
  318. QList< Tomahawk::result_ptr > results;
  319. foreach( const QVariant& rv, reslist )
  320. {
  321. QVariantMap m = rv.toMap();
  322. if ( m.value( "artist" ).toString().trimmed().isEmpty() || m.value( "track" ).toString().trimmed().isEmpty() )
  323. continue;
  324. // TODO we need to handle preview urls separately. they should never trump a real url, and we need to display
  325. // the purchaseUrl for the user to upgrade to a full stream.
  326. if ( m.value( "preview" ).toBool() == true )
  327. continue;
  328. Tomahawk::result_ptr rp = Tomahawk::Result::get( m.value( "url" ).toString() );
  329. Tomahawk::artist_ptr ap = Tomahawk::Artist::get( m.value( "artist" ).toString(), false );
  330. rp->setArtist( ap );
  331. rp->setAlbum( Tomahawk::Album::get( ap, m.value( "album" ).toString(), false ) );
  332. rp->setTrack( m.value( "track" ).toString() );
  333. rp->setAlbumPos( m.value( "albumpos" ).toUInt() );
  334. rp->setBitrate( m.value( "bitrate" ).toUInt() );
  335. rp->setSize( m.value( "size" ).toUInt() );
  336. rp->setRID( uuid() );
  337. rp->setFriendlySource( name() );
  338. rp->setPurchaseUrl( m.value( "purchaseUrl" ).toString() );
  339. rp->setLinkUrl( m.value( "linkUrl" ).toString() );
  340. rp->setScore( m.value( "score" ).toFloat() );
  341. rp->setDiscNumber( m.value( "discnumber" ).toUInt() );
  342. if ( m.contains( "year" ) )
  343. {
  344. QVariantMap attr;
  345. attr[ "releaseyear" ] = m.value( "year" );
  346. rp->setAttributes( attr );
  347. }
  348. rp->setDuration( m.value( "duration", 0 ).toUInt() );
  349. if ( rp->duration() <= 0 && m.contains( "durationString" ) )
  350. {
  351. QTime time = QTime::fromString( m.value( "durationString" ).toString(), "hh:mm:ss" );
  352. rp->setDuration( time.secsTo( QTime( 0, 0 ) ) * -1 );
  353. }
  354. rp->setMimetype( m.value( "mimetype" ).toString() );
  355. if ( rp->mimetype().isEmpty() )
  356. {
  357. rp->setMimetype( TomahawkUtils::extensionToMimetype( m.value( "extension" ).toString() ) );
  358. Q_ASSERT( !rp->mimetype().isEmpty() );
  359. }
  360. rp->setResolvedBy( this );
  361. results << rp;
  362. }
  363. return results;
  364. }
  365. void
  366. QtScriptResolver::stop()
  367. {
  368. m_stopped = true;
  369. Tomahawk::Pipeline::instance()->removeResolver( this );
  370. emit stopped();
  371. }
  372. void
  373. QtScriptResolver::loadUi()
  374. {
  375. QVariantMap m = m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE "resolver.getConfigUi();" ).toMap();
  376. m_dataWidgets = m["fields"].toList();
  377. bool compressed = m.value( "compressed", "false" ).toBool();
  378. qDebug() << "Resolver has a preferences widget! compressed?" << compressed;
  379. QByteArray uiData = m[ "widget" ].toByteArray();
  380. if( compressed )
  381. uiData = qUncompress( QByteArray::fromBase64( uiData ) );
  382. else
  383. uiData = QByteArray::fromBase64( uiData );
  384. QVariantMap images;
  385. foreach(const QVariant& item, m[ "images" ].toList())
  386. {
  387. QString key = item.toMap().keys().first();
  388. QVariant value = item.toMap().value(key);
  389. images[key] = value;
  390. }
  391. if( m.contains( "images" ) )
  392. uiData = fixDataImagePaths( uiData, compressed, images );
  393. m_configWidget = QWeakPointer< QWidget >( widgetFromData( uiData, 0 ) );
  394. emit changed();
  395. }
  396. QWidget*
  397. QtScriptResolver::configUI() const
  398. {
  399. if( m_configWidget.isNull() )
  400. return 0;
  401. else
  402. return m_configWidget.data();
  403. }
  404. void
  405. QtScriptResolver::saveConfig()
  406. {
  407. QVariant saveData = loadDataFromWidgets();
  408. // qDebug() << Q_FUNC_INFO << saveData;
  409. m_resolverHelper->setResolverConfig( saveData.toMap() );
  410. m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE "resolver.saveUserConfig();" );
  411. }
  412. QWidget*
  413. QtScriptResolver::findWidget(QWidget* widget, const QString& objectName)
  414. {
  415. if( !widget || !widget->isWidgetType() )
  416. return 0;
  417. if( widget->objectName() == objectName )
  418. return widget;
  419. foreach( QObject* child, widget->children() )
  420. {
  421. QWidget* found = findWidget(qobject_cast< QWidget* >( child ), objectName);
  422. if( found )
  423. return found;
  424. }
  425. return 0;
  426. }
  427. QVariant
  428. QtScriptResolver::widgetData(QWidget* widget, const QString& property)
  429. {
  430. for( int i = 0; i < widget->metaObject()->propertyCount(); i++ )
  431. {
  432. if( widget->metaObject()->property( i ).name() == property )
  433. {
  434. return widget->property( property.toLatin1() );
  435. }
  436. }
  437. return QVariant();
  438. }
  439. void
  440. QtScriptResolver::setWidgetData(const QVariant& value, QWidget* widget, const QString& property)
  441. {
  442. for( int i = 0; i < widget->metaObject()->propertyCount(); i++ )
  443. {
  444. if( widget->metaObject()->property( i ).name() == property )
  445. {
  446. widget->metaObject()->property( i ).write( widget, value);
  447. return;
  448. }
  449. }
  450. }
  451. QVariantMap
  452. QtScriptResolver::loadDataFromWidgets()
  453. {
  454. QVariantMap saveData;
  455. foreach(const QVariant& dataWidget, m_dataWidgets)
  456. {
  457. QVariantMap data = dataWidget.toMap();
  458. QString widgetName = data["widget"].toString();
  459. QWidget* widget= findWidget( m_configWidget.data(), widgetName );
  460. QVariant value = widgetData( widget, data["property"].toString() );
  461. saveData[ data["name"].toString() ] = value;
  462. }
  463. return saveData;
  464. }
  465. void
  466. QtScriptResolver::fillDataInWidgets( const QVariantMap& data )
  467. {
  468. foreach(const QVariant& dataWidget, m_dataWidgets)
  469. {
  470. QString widgetName = dataWidget.toMap()["widget"].toString();
  471. QWidget* widget= findWidget( m_configWidget.data(), widgetName );
  472. if( !widget )
  473. {
  474. tLog() << Q_FUNC_INFO << "Widget specified in resolver was not found:" << widgetName;
  475. Q_ASSERT(false);
  476. return;
  477. }
  478. QString propertyName = dataWidget.toMap()["property"].toString();
  479. QString name = dataWidget.toMap()["name"].toString();
  480. setWidgetData( data[ name ], widget, propertyName );
  481. }
  482. }
  483. QVariantMap
  484. QtScriptResolver::resolverSettings()
  485. {
  486. return m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE "if(resolver.settings) resolver.settings; else getSettings(); " ).toMap();
  487. }
  488. QVariantMap
  489. QtScriptResolver::resolverUserConfig()
  490. {
  491. return m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE "resolver.getUserConfig();" ).toMap();
  492. }
  493. QVariantMap
  494. QtScriptResolver::resolverInit()
  495. {
  496. return m_engine->mainFrame()->evaluateJavaScript( RESOLVER_LEGACY_CODE "resolver.init();" ).toMap();
  497. }