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