/src/libtomahawk/Source.cpp

http://github.com/tomahawk-player/tomahawk · C++ · 837 lines · 616 code · 181 blank · 40 comment · 78 complexity · 68a3d8cf94bd428b1835bb31c9d761ee MD5 · raw file

  1. /* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
  2. *
  3. * Copyright 2010-2015, Christian Muehlhaeuser <muesli@tomahawk-player.org>
  4. * Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
  5. * Copyright 2013, Uwe L. Korn <uwelk@xhochy.com>
  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 "Source_p.h"
  21. #include "collection/Collection.h"
  22. #include "SourceList.h"
  23. #include "SourcePlaylistInterface.h"
  24. #include "accounts/AccountManager.h"
  25. #include "network/ControlConnection.h"
  26. #include "database/DatabaseCommand_AddSource.h"
  27. #include "database/DatabaseCommand_CollectionStats.h"
  28. #include "database/DatabaseCommand_LoadAllSources.h"
  29. #include "database/DatabaseCommand_SocialAction.h"
  30. #include "database/DatabaseCommand_SourceOffline.h"
  31. #include "database/DatabaseCommand_UpdateSearchIndex.h"
  32. #include "database/DatabaseImpl.h"
  33. #include "database/Database.h"
  34. #include "utils/Logger.h"
  35. #include "sip/PeerInfo.h"
  36. #include "utils/TomahawkCache.h"
  37. #include "utils/TomahawkUtilsGui.h"
  38. #include <QCoreApplication>
  39. #include <QtAlgorithms>
  40. #include <QPainter>
  41. using namespace Tomahawk;
  42. Source::Source( int id, const QString& nodeId )
  43. : QObject()
  44. , d_ptr( new SourcePrivate( this, id, nodeId ) )
  45. {
  46. Q_D( Source );
  47. d->scrubFriendlyName = qApp->arguments().contains( "--demo" );
  48. d->isLocal = ( id == 0 );
  49. d->currentTrackTimer.setSingleShot( true );
  50. connect( &d->currentTrackTimer, SIGNAL( timeout() ), this, SLOT( trackTimerFired() ) );
  51. if ( d->isLocal )
  52. {
  53. connect( Accounts::AccountManager::instance(),
  54. SIGNAL( connected( Tomahawk::Accounts::Account* ) ),
  55. SLOT( setOnline() ) );
  56. connect( Accounts::AccountManager::instance(),
  57. SIGNAL( disconnected( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason ) ),
  58. SLOT( handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason ) ) );
  59. }
  60. }
  61. Source::~Source()
  62. {
  63. tDebug() << Q_FUNC_INFO << friendlyName();
  64. delete d_ptr;
  65. }
  66. bool
  67. Source::isLocal() const
  68. {
  69. Q_D( const Source );
  70. return d->isLocal;
  71. }
  72. bool
  73. Source::isOnline() const
  74. {
  75. Q_D( const Source );
  76. return d->online || d->isLocal;
  77. }
  78. bool
  79. Source::setControlConnection( ControlConnection* cc )
  80. {
  81. Q_D( Source );
  82. QMutexLocker locker( &d->setControlConnectionMutex );
  83. if ( !d->cc.isNull() && d->cc->isReady() && d->cc->isRunning() )
  84. {
  85. const QString& nodeid = Database::instance()->impl()->dbid();
  86. peerInfoDebug( (*cc->peerInfos().begin()) ) << Q_FUNC_INFO
  87. << "Comparing" << cc->id()
  88. << "and" << nodeid
  89. << "to detect duplicate connections"
  90. << "outbound:" << cc->outbound();
  91. // If our nodeid is "higher" than the other, we prefer inbound connection, else outbound.
  92. if ( ( cc->id() < nodeid && d->cc->outbound() ) || ( cc->id() > nodeid && !d->cc->outbound() ) )
  93. {
  94. // Tell the ControlConnection it is not anymore responsible for us.
  95. d->cc->unbindFromSource();
  96. // This ControlConnection is not needed anymore, get rid of it!
  97. // (But decouple the deletion it from the current activity)
  98. QMetaObject::invokeMethod( d->cc.data(), "deleteLater", Qt::QueuedConnection);
  99. // Use new ControlConnection
  100. d->cc = cc;
  101. return true;
  102. }
  103. else
  104. {
  105. return false;
  106. }
  107. }
  108. else
  109. {
  110. d->cc = cc;
  111. return true;
  112. }
  113. }
  114. const QSet<peerinfo_ptr>
  115. Source::peerInfos() const
  116. {
  117. if ( controlConnection() )
  118. {
  119. return controlConnection()->peerInfos();
  120. }
  121. else if ( isLocal() )
  122. {
  123. return PeerInfo::getAllSelf().toSet();
  124. }
  125. return QSet< Tomahawk::peerinfo_ptr >();
  126. }
  127. collection_ptr
  128. Source::dbCollection() const
  129. {
  130. Q_D( const Source );
  131. if ( !d->collections.isEmpty() )
  132. {
  133. foreach ( const collection_ptr& collection, d->collections )
  134. {
  135. if ( collection->backendType() == Collection::DatabaseCollectionType )
  136. {
  137. return collection; // We assume only one is a db collection. Now get off my lawn.
  138. }
  139. }
  140. }
  141. return collection_ptr();
  142. }
  143. QList<collection_ptr>
  144. Source::collections() const
  145. {
  146. return d_func()->collections;
  147. }
  148. void
  149. Source::setStats( const QVariantMap& m )
  150. {
  151. Q_D( Source );
  152. d->stats = m;
  153. emit stats( d->stats );
  154. emit stateChanged();
  155. }
  156. QString
  157. Source::nodeId() const
  158. {
  159. return d_func()->nodeId;
  160. }
  161. QString
  162. Source::prettyName( const QString& name ) const
  163. {
  164. Q_D( const Source );
  165. if ( d->scrubFriendlyName )
  166. {
  167. if ( name.indexOf( "@" ) > 0 )
  168. {
  169. return name.split( "@" ).first();
  170. }
  171. }
  172. return name;
  173. }
  174. QString
  175. Source::friendlyName() const
  176. {
  177. Q_D( const Source );
  178. QStringList candidateNames;
  179. foreach ( const peerinfo_ptr& peerInfo, peerInfos() )
  180. {
  181. if ( !peerInfo.isNull() && !peerInfo->friendlyName().isEmpty() )
  182. {
  183. candidateNames.append( peerInfo->friendlyName() );
  184. }
  185. }
  186. if ( !candidateNames.isEmpty() )
  187. {
  188. if ( candidateNames.count() > 1 )
  189. qSort( candidateNames.begin(), candidateNames.end(), &Source::friendlyNamesLessThan );
  190. return prettyName( candidateNames.first() );
  191. }
  192. if ( d->friendlyname.isEmpty() )
  193. {
  194. return prettyName( dbFriendlyName() );
  195. }
  196. return prettyName( d->friendlyname );
  197. }
  198. bool
  199. Source::friendlyNamesLessThan( const QString& first, const QString& second )
  200. {
  201. //Least favored match first.
  202. QList< QRegExp > penalties;
  203. penalties.append( QRegExp( "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}" ) ); //IPv4 address
  204. penalties.append( QRegExp( "([\\w-\\.\\+]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})" ) ); //email/jabber id
  205. //Most favored match first.
  206. QList< QRegExp > favored;
  207. favored.append( QRegExp( "\\b([A-Z][a-z']* ?){2,10}" ) ); //properly capitalized person's name
  208. favored.append( QRegExp( "[a-zA-Z ']+" ) ); //kind of person's name
  209. //We check if the strings match the regexps. The regexps represent friendly name patterns we do
  210. //*not* want (penalties) or want (favored), prioritized. If none of the strings match a regexp,
  211. //we go to the next regexp. If one of the strings matches, and we're matching penalties, we say
  212. //the other one is lessThan, i.e. comes first. If one of the string matches, and we're matching
  213. //favored, we say this one is lessThan, i.e. comes first. If both strings match, or if no match
  214. //is found for any regexp, we go to string comparison (fallback).
  215. while( !penalties.isEmpty() || !favored.isEmpty() )
  216. {
  217. QRegExp rx;
  218. bool isPenalty;
  219. if ( !penalties.isEmpty() )
  220. {
  221. rx = penalties.first();
  222. penalties.pop_front();
  223. isPenalty = true;
  224. }
  225. else
  226. {
  227. rx = favored.first();
  228. favored.pop_front();
  229. isPenalty = false;
  230. }
  231. const bool matchFirst = rx.exactMatch( first );
  232. const bool matchSecond = rx.exactMatch( second );
  233. if ( !matchFirst && !matchSecond )
  234. continue;
  235. if ( matchFirst && matchSecond )
  236. break;
  237. if ( matchFirst && !matchSecond )
  238. return !isPenalty;
  239. if ( !matchFirst && matchSecond)
  240. return isPenalty;
  241. }
  242. return first.compare( second ) == -1;
  243. }
  244. QPixmap
  245. Source::avatar( TomahawkUtils::ImageMode style, const QSize& size, bool defaultAvatarFallback )
  246. {
  247. Q_D( Source );
  248. foreach ( const peerinfo_ptr& peerInfo, peerInfos() )
  249. {
  250. if ( peerInfo && !peerInfo->avatar( style, size ).isNull() )
  251. {
  252. return peerInfo->avatar( style, size );
  253. }
  254. }
  255. // Try to get the avatar from the cache
  256. // Hint: We store the avatar for each xmpp peer using its contactId, the dbFriendlyName is a contactId of a peer
  257. if ( !d->avatarLoaded )
  258. {
  259. d->avatarLoaded = true;
  260. QByteArray avatarBuffer = TomahawkUtils::Cache::instance()->getData( "Sources", dbFriendlyName() ).toByteArray();
  261. if ( !avatarBuffer.isNull() )
  262. {
  263. QPixmap avatar;
  264. avatar.loadFromData( avatarBuffer );
  265. avatarBuffer.clear();
  266. d->avatar = new QPixmap( TomahawkUtils::createRoundedImage( avatar, QSize( 0, 0 ) ) );
  267. }
  268. }
  269. if ( d->avatarLoaded && d->avatar )
  270. {
  271. return d->avatar->scaled( size, Qt::KeepAspectRatio, Qt::SmoothTransformation );
  272. }
  273. if ( defaultAvatarFallback )
  274. {
  275. QPixmap px = TomahawkUtils::defaultPixmap( TomahawkUtils::DefaultSourceAvatar, style, size );
  276. QPainter p( &px );
  277. p.setRenderHint( QPainter::Antialiasing );
  278. QFont f = p.font();
  279. f.setPixelSize( px.size().height() - 8 );
  280. p.setFont( f );
  281. p.setPen( Qt::white );
  282. const QString initial = friendlyName().left( 1 ).toUpper();
  283. const QFontMetricsF fm( f );
  284. const qreal w = fm.width( initial );
  285. const QPointF pxp = QPointF( px.rect().topLeft() ) + QPointF( px.rect().width() / 2.0 - w / 2.0, px.rect().height() / 2.0 - fm.height() / 2.0 + fm.ascent() );
  286. p.drawText( pxp, initial );
  287. return px;
  288. }
  289. else
  290. return QPixmap();
  291. }
  292. void
  293. Source::setFriendlyName( const QString& fname )
  294. {
  295. Q_D( Source );
  296. if ( fname.isEmpty() )
  297. {
  298. return;
  299. }
  300. d->friendlyname = fname;
  301. }
  302. QString
  303. Source::dbFriendlyName() const
  304. {
  305. Q_D( const Source );
  306. if ( d->dbFriendlyName.isEmpty() )
  307. {
  308. return nodeId();
  309. }
  310. return d->dbFriendlyName;
  311. }
  312. void
  313. Source::setDbFriendlyName( const QString& dbFriendlyName )
  314. {
  315. Q_D( Source );
  316. if ( dbFriendlyName.isEmpty() )
  317. return;
  318. d->dbFriendlyName = dbFriendlyName;
  319. }
  320. void
  321. Source::addCollection( const collection_ptr& c )
  322. {
  323. Q_D( Source );
  324. //Q_ASSERT( m_collections.isEmpty() ); // only 1 source supported atm
  325. d->collections.append( c );
  326. emit collectionAdded( c );
  327. }
  328. void
  329. Source::removeCollection( const collection_ptr& c )
  330. {
  331. Q_D( Source );
  332. //Q_ASSERT( m_collections.length() == 1 && m_collections.first() == c ); // only 1 source supported atm
  333. d->collections.removeAll( c );
  334. emit collectionRemoved( c );
  335. }
  336. int
  337. Source::id() const
  338. {
  339. Q_D( const Source );
  340. return d->id;
  341. }
  342. ControlConnection*
  343. Source::controlConnection() const
  344. {
  345. Q_D( const Source );
  346. return d->cc.data();
  347. }
  348. void
  349. Source::handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason reason )
  350. {
  351. if ( reason == Tomahawk::Accounts::AccountManager::Disabled )
  352. setOffline();
  353. }
  354. void
  355. Source::setOffline()
  356. {
  357. Q_D( Source );
  358. qDebug() << Q_FUNC_INFO << friendlyName();
  359. if ( !d->online )
  360. return;
  361. d->online = false;
  362. emit offline();
  363. if ( !isLocal() )
  364. {
  365. d->currentTrack.clear();
  366. emit stateChanged();
  367. d->cc = 0;
  368. DatabaseCommand_SourceOffline* cmd = new DatabaseCommand_SourceOffline( id() );
  369. Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
  370. }
  371. }
  372. void
  373. Source::setOnline( bool force )
  374. {
  375. Q_D( Source );
  376. tDebug( LOGVERBOSE ) << Q_FUNC_INFO << friendlyName();
  377. if ( d->online && !force )
  378. return;
  379. d->online = true;
  380. emit online();
  381. if ( !isLocal() )
  382. {
  383. // ensure username is in the database
  384. DatabaseCommand_addSource* cmd = new DatabaseCommand_addSource( d->nodeId, dbFriendlyName() );
  385. connect( cmd, SIGNAL( done( unsigned int, QString ) ),
  386. SLOT( dbLoaded( unsigned int, const QString& ) ) );
  387. Database::instance()->enqueue( Tomahawk::dbcmd_ptr(cmd) );
  388. }
  389. }
  390. void
  391. Source::dbLoaded( unsigned int id, const QString& fname )
  392. {
  393. Q_D( Source );
  394. d->id = id;
  395. setDbFriendlyName( fname );
  396. emit syncedWithDatabase();
  397. }
  398. void
  399. Source::scanningFinished( bool updateGUI )
  400. {
  401. Q_D( Source );
  402. d->textStatus = QString();
  403. if ( d->updateIndexWhenSynced )
  404. {
  405. d->updateIndexWhenSynced = false;
  406. updateTracks();
  407. }
  408. emit stateChanged();
  409. if ( updateGUI )
  410. emit synced();
  411. }
  412. void
  413. Source::onStateChanged( Tomahawk::DBSyncConnectionState newstate, Tomahawk::DBSyncConnectionState oldstate, const QString& info )
  414. {
  415. Q_D( Source );
  416. Q_UNUSED( oldstate );
  417. QString msg;
  418. switch( newstate )
  419. {
  420. case CHECKING:
  421. {
  422. msg = tr( "Checking" );
  423. break;
  424. }
  425. case FETCHING:
  426. {
  427. msg = tr( "Syncing" );
  428. break;
  429. }
  430. case PARSING:
  431. {
  432. msg = tr( "Importing" );
  433. break;
  434. }
  435. case SCANNING:
  436. {
  437. msg = tr( "Scanning (%L1 tracks)" ).arg( info );
  438. break;
  439. }
  440. case SYNCED:
  441. {
  442. msg = QString();
  443. break;
  444. }
  445. default:
  446. msg = QString();
  447. }
  448. d->state = newstate;
  449. d->textStatus = msg;
  450. emit stateChanged();
  451. }
  452. unsigned int
  453. Source::trackCount() const
  454. {
  455. Q_D( const Source );
  456. return d->stats.value( "numfiles", 0 ).toUInt();
  457. }
  458. query_ptr
  459. Source::currentTrack() const
  460. {
  461. Q_D( const Source );
  462. return d->currentTrack;
  463. }
  464. Tomahawk::playlistinterface_ptr
  465. Source::playlistInterface()
  466. {
  467. Q_D( Source );
  468. if ( d->playlistInterface.isNull() )
  469. {
  470. Tomahawk::source_ptr source = SourceList::instance()->get( id() );
  471. d->playlistInterface = Tomahawk::playlistinterface_ptr( new Tomahawk::SourcePlaylistInterface( source.data() ) );
  472. }
  473. return d->playlistInterface;
  474. }
  475. QSharedPointer<QMutexLocker>
  476. Source::acquireLock()
  477. {
  478. Q_D( Source );
  479. return QSharedPointer<QMutexLocker>( new QMutexLocker( &d->mutex ) );
  480. }
  481. void
  482. Source::onPlaybackStarted( const Tomahawk::track_ptr& track, unsigned int duration )
  483. {
  484. Q_D( Source );
  485. tLog( LOGVERBOSE ) << Q_FUNC_INFO << track->toString();
  486. d->currentTrack = track->toQuery();
  487. d->currentTrackTimer.start( duration * 1000 + 900000 ); // duration comes in seconds
  488. if ( d->playlistInterface.isNull() )
  489. playlistInterface();
  490. emit playbackStarted( track );
  491. emit stateChanged();
  492. }
  493. void
  494. Source::onPlaybackFinished( const Tomahawk::track_ptr& track, const Tomahawk::PlaybackLog& log )
  495. {
  496. Q_D( Source );
  497. tDebug( LOGVERBOSE ) << Q_FUNC_INFO << track->toString();
  498. emit playbackFinished( track, log );
  499. d->currentTrack.clear();
  500. emit stateChanged();
  501. }
  502. void
  503. Source::trackTimerFired()
  504. {
  505. Q_D( Source );
  506. d->currentTrack.clear();
  507. emit stateChanged();
  508. }
  509. QString
  510. Source::lastCmdGuid() const
  511. {
  512. Q_D( const Source );
  513. QMutexLocker lock( &d->cmdMutex );
  514. return d->lastCmdGuid;
  515. }
  516. void
  517. Source::setLastCmdGuid( const QString& guid )
  518. {
  519. Q_D( Source );
  520. tLog( LOGVERBOSE ) << Q_FUNC_INFO << "name is" << friendlyName() << "and guid is" << guid;
  521. QMutexLocker lock( &d->cmdMutex );
  522. d->lastCmdGuid = guid;
  523. }
  524. void
  525. Source::addCommand( const dbcmd_ptr& command )
  526. {
  527. Q_D( Source );
  528. QMutexLocker lock( &d->cmdMutex );
  529. d->cmds << command;
  530. if ( !command->singletonCmd() )
  531. {
  532. d->lastCmdGuid = command->guid();
  533. }
  534. d->commandCount = d->cmds.count();
  535. }
  536. void
  537. Source::executeCommands()
  538. {
  539. Q_D( Source );
  540. if ( QThread::currentThread() != thread() )
  541. {
  542. QMetaObject::invokeMethod( this, "executeCommands", Qt::QueuedConnection );
  543. return;
  544. }
  545. bool commandsAvail = false;
  546. {
  547. QMutexLocker lock( &d->cmdMutex );
  548. commandsAvail = !d->cmds.isEmpty();
  549. }
  550. if ( commandsAvail )
  551. {
  552. QMutexLocker lock( &d->cmdMutex );
  553. QList< Tomahawk::dbcmd_ptr > cmdGroup;
  554. Tomahawk::dbcmd_ptr cmd = d->cmds.takeFirst();
  555. while ( cmd->groupable() )
  556. {
  557. cmdGroup << cmd;
  558. if ( !d->cmds.isEmpty() && d->cmds.first()->groupable() && d->cmds.first()->commandname() == cmd->commandname() )
  559. cmd = d->cmds.takeFirst();
  560. else
  561. break;
  562. }
  563. // return here when the last command finished
  564. connect( cmd.data(), SIGNAL( finished() ), SLOT( executeCommands() ) );
  565. if ( !cmdGroup.isEmpty() )
  566. {
  567. Database::instance()->enqueue( cmdGroup );
  568. }
  569. else
  570. {
  571. Database::instance()->enqueue( cmd );
  572. }
  573. int percentage = ( float( d->commandCount - d->cmds.count() ) / (float)d->commandCount ) * 100.0;
  574. d->textStatus = tr( "Saving (%1%)" ).arg( percentage );
  575. emit stateChanged();
  576. }
  577. else
  578. {
  579. if ( d->updateIndexWhenSynced )
  580. {
  581. d->updateIndexWhenSynced = false;
  582. updateTracks();
  583. }
  584. d->textStatus = QString();
  585. d->state = SYNCED;
  586. emit commandsFinished();
  587. emit stateChanged();
  588. emit synced();
  589. }
  590. }
  591. void
  592. Source::reportSocialAttributesChanged( DatabaseCommand_SocialAction* action )
  593. {
  594. Q_ASSERT( action );
  595. emit socialAttributesChanged( action->action() );
  596. if ( action->action() == "latchOn" )
  597. {
  598. const source_ptr to = SourceList::instance()->get( action->comment() );
  599. if ( !to.isNull() )
  600. emit latchedOn( to );
  601. }
  602. else if ( action->action() == "latchOff" )
  603. {
  604. const source_ptr from = SourceList::instance()->get( action->comment() );
  605. if ( !from.isNull() )
  606. emit latchedOff( from );
  607. }
  608. }
  609. void
  610. Source::updateTracks()
  611. {
  612. {
  613. DatabaseCommand* cmd = new DatabaseCommand_UpdateSearchIndex();
  614. Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
  615. }
  616. {
  617. // Re-calculate local db stats
  618. DatabaseCommand_CollectionStats* cmd = new DatabaseCommand_CollectionStats( SourceList::instance()->get( id() ) );
  619. connect( cmd, SIGNAL( done( QVariantMap ) ), SLOT( setStats( QVariantMap ) ), Qt::QueuedConnection );
  620. Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
  621. }
  622. }
  623. void
  624. Source::updateIndexWhenSynced()
  625. {
  626. Q_D( Source );
  627. d->updateIndexWhenSynced = true;
  628. }
  629. QString
  630. Source::textStatus() const
  631. {
  632. Q_D( const Source );
  633. if ( !d->textStatus.isEmpty() )
  634. {
  635. return d->textStatus;
  636. }
  637. if ( !currentTrack().isNull() )
  638. {
  639. return currentTrack()->queryTrack()->track() + " - " + currentTrack()->queryTrack()->artist();
  640. }
  641. // do not use isOnline() here - it will always return true for the local source
  642. if ( d->online )
  643. {
  644. return tr( "Online" );
  645. }
  646. else
  647. {
  648. return tr( "Offline" );
  649. }
  650. }
  651. DBSyncConnectionState
  652. Source::state() const
  653. {
  654. Q_D( const Source );
  655. return d->state;
  656. }