/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
- /* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
- *
- * Copyright 2010-2015, Christian Muehlhaeuser <muesli@tomahawk-player.org>
- * Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
- * Copyright 2013, Uwe L. Korn <uwelk@xhochy.com>
- *
- * Tomahawk is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Tomahawk is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
- */
- #include "Source_p.h"
- #include "collection/Collection.h"
- #include "SourceList.h"
- #include "SourcePlaylistInterface.h"
- #include "accounts/AccountManager.h"
- #include "network/ControlConnection.h"
- #include "database/DatabaseCommand_AddSource.h"
- #include "database/DatabaseCommand_CollectionStats.h"
- #include "database/DatabaseCommand_LoadAllSources.h"
- #include "database/DatabaseCommand_SocialAction.h"
- #include "database/DatabaseCommand_SourceOffline.h"
- #include "database/DatabaseCommand_UpdateSearchIndex.h"
- #include "database/DatabaseImpl.h"
- #include "database/Database.h"
- #include "utils/Logger.h"
- #include "sip/PeerInfo.h"
- #include "utils/TomahawkCache.h"
- #include "utils/TomahawkUtilsGui.h"
- #include <QCoreApplication>
- #include <QtAlgorithms>
- #include <QPainter>
- using namespace Tomahawk;
- Source::Source( int id, const QString& nodeId )
- : QObject()
- , d_ptr( new SourcePrivate( this, id, nodeId ) )
- {
- Q_D( Source );
- d->scrubFriendlyName = qApp->arguments().contains( "--demo" );
- d->isLocal = ( id == 0 );
- d->currentTrackTimer.setSingleShot( true );
- connect( &d->currentTrackTimer, SIGNAL( timeout() ), this, SLOT( trackTimerFired() ) );
- if ( d->isLocal )
- {
- connect( Accounts::AccountManager::instance(),
- SIGNAL( connected( Tomahawk::Accounts::Account* ) ),
- SLOT( setOnline() ) );
- connect( Accounts::AccountManager::instance(),
- SIGNAL( disconnected( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason ) ),
- SLOT( handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason ) ) );
- }
- }
- Source::~Source()
- {
- tDebug() << Q_FUNC_INFO << friendlyName();
- delete d_ptr;
- }
- bool
- Source::isLocal() const
- {
- Q_D( const Source );
- return d->isLocal;
- }
- bool
- Source::isOnline() const
- {
- Q_D( const Source );
- return d->online || d->isLocal;
- }
- bool
- Source::setControlConnection( ControlConnection* cc )
- {
- Q_D( Source );
- QMutexLocker locker( &d->setControlConnectionMutex );
- if ( !d->cc.isNull() && d->cc->isReady() && d->cc->isRunning() )
- {
- const QString& nodeid = Database::instance()->impl()->dbid();
- peerInfoDebug( (*cc->peerInfos().begin()) ) << Q_FUNC_INFO
- << "Comparing" << cc->id()
- << "and" << nodeid
- << "to detect duplicate connections"
- << "outbound:" << cc->outbound();
- // If our nodeid is "higher" than the other, we prefer inbound connection, else outbound.
- if ( ( cc->id() < nodeid && d->cc->outbound() ) || ( cc->id() > nodeid && !d->cc->outbound() ) )
- {
- // Tell the ControlConnection it is not anymore responsible for us.
- d->cc->unbindFromSource();
- // This ControlConnection is not needed anymore, get rid of it!
- // (But decouple the deletion it from the current activity)
- QMetaObject::invokeMethod( d->cc.data(), "deleteLater", Qt::QueuedConnection);
- // Use new ControlConnection
- d->cc = cc;
- return true;
- }
- else
- {
- return false;
- }
- }
- else
- {
- d->cc = cc;
- return true;
- }
- }
- const QSet<peerinfo_ptr>
- Source::peerInfos() const
- {
- if ( controlConnection() )
- {
- return controlConnection()->peerInfos();
- }
- else if ( isLocal() )
- {
- return PeerInfo::getAllSelf().toSet();
- }
- return QSet< Tomahawk::peerinfo_ptr >();
- }
- collection_ptr
- Source::dbCollection() const
- {
- Q_D( const Source );
- if ( !d->collections.isEmpty() )
- {
- foreach ( const collection_ptr& collection, d->collections )
- {
- if ( collection->backendType() == Collection::DatabaseCollectionType )
- {
- return collection; // We assume only one is a db collection. Now get off my lawn.
- }
- }
- }
- return collection_ptr();
- }
- QList<collection_ptr>
- Source::collections() const
- {
- return d_func()->collections;
- }
- void
- Source::setStats( const QVariantMap& m )
- {
- Q_D( Source );
- d->stats = m;
- emit stats( d->stats );
- emit stateChanged();
- }
- QString
- Source::nodeId() const
- {
- return d_func()->nodeId;
- }
- QString
- Source::prettyName( const QString& name ) const
- {
- Q_D( const Source );
- if ( d->scrubFriendlyName )
- {
- if ( name.indexOf( "@" ) > 0 )
- {
- return name.split( "@" ).first();
- }
- }
- return name;
- }
- QString
- Source::friendlyName() const
- {
- Q_D( const Source );
- QStringList candidateNames;
- foreach ( const peerinfo_ptr& peerInfo, peerInfos() )
- {
- if ( !peerInfo.isNull() && !peerInfo->friendlyName().isEmpty() )
- {
- candidateNames.append( peerInfo->friendlyName() );
- }
- }
- if ( !candidateNames.isEmpty() )
- {
- if ( candidateNames.count() > 1 )
- qSort( candidateNames.begin(), candidateNames.end(), &Source::friendlyNamesLessThan );
- return prettyName( candidateNames.first() );
- }
- if ( d->friendlyname.isEmpty() )
- {
- return prettyName( dbFriendlyName() );
- }
- return prettyName( d->friendlyname );
- }
- bool
- Source::friendlyNamesLessThan( const QString& first, const QString& second )
- {
- //Least favored match first.
- QList< QRegExp > penalties;
- penalties.append( QRegExp( "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}" ) ); //IPv4 address
- penalties.append( QRegExp( "([\\w-\\.\\+]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})" ) ); //email/jabber id
- //Most favored match first.
- QList< QRegExp > favored;
- favored.append( QRegExp( "\\b([A-Z][a-z']* ?){2,10}" ) ); //properly capitalized person's name
- favored.append( QRegExp( "[a-zA-Z ']+" ) ); //kind of person's name
- //We check if the strings match the regexps. The regexps represent friendly name patterns we do
- //*not* want (penalties) or want (favored), prioritized. If none of the strings match a regexp,
- //we go to the next regexp. If one of the strings matches, and we're matching penalties, we say
- //the other one is lessThan, i.e. comes first. If one of the string matches, and we're matching
- //favored, we say this one is lessThan, i.e. comes first. If both strings match, or if no match
- //is found for any regexp, we go to string comparison (fallback).
- while( !penalties.isEmpty() || !favored.isEmpty() )
- {
- QRegExp rx;
- bool isPenalty;
- if ( !penalties.isEmpty() )
- {
- rx = penalties.first();
- penalties.pop_front();
- isPenalty = true;
- }
- else
- {
- rx = favored.first();
- favored.pop_front();
- isPenalty = false;
- }
- const bool matchFirst = rx.exactMatch( first );
- const bool matchSecond = rx.exactMatch( second );
- if ( !matchFirst && !matchSecond )
- continue;
- if ( matchFirst && matchSecond )
- break;
- if ( matchFirst && !matchSecond )
- return !isPenalty;
- if ( !matchFirst && matchSecond)
- return isPenalty;
- }
- return first.compare( second ) == -1;
- }
- QPixmap
- Source::avatar( TomahawkUtils::ImageMode style, const QSize& size, bool defaultAvatarFallback )
- {
- Q_D( Source );
- foreach ( const peerinfo_ptr& peerInfo, peerInfos() )
- {
- if ( peerInfo && !peerInfo->avatar( style, size ).isNull() )
- {
- return peerInfo->avatar( style, size );
- }
- }
- // Try to get the avatar from the cache
- // Hint: We store the avatar for each xmpp peer using its contactId, the dbFriendlyName is a contactId of a peer
- if ( !d->avatarLoaded )
- {
- d->avatarLoaded = true;
- QByteArray avatarBuffer = TomahawkUtils::Cache::instance()->getData( "Sources", dbFriendlyName() ).toByteArray();
- if ( !avatarBuffer.isNull() )
- {
- QPixmap avatar;
- avatar.loadFromData( avatarBuffer );
- avatarBuffer.clear();
- d->avatar = new QPixmap( TomahawkUtils::createRoundedImage( avatar, QSize( 0, 0 ) ) );
- }
- }
- if ( d->avatarLoaded && d->avatar )
- {
- return d->avatar->scaled( size, Qt::KeepAspectRatio, Qt::SmoothTransformation );
- }
- if ( defaultAvatarFallback )
- {
- QPixmap px = TomahawkUtils::defaultPixmap( TomahawkUtils::DefaultSourceAvatar, style, size );
- QPainter p( &px );
- p.setRenderHint( QPainter::Antialiasing );
- QFont f = p.font();
- f.setPixelSize( px.size().height() - 8 );
- p.setFont( f );
- p.setPen( Qt::white );
- const QString initial = friendlyName().left( 1 ).toUpper();
- const QFontMetricsF fm( f );
- const qreal w = fm.width( initial );
- 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() );
- p.drawText( pxp, initial );
- return px;
- }
- else
- return QPixmap();
- }
- void
- Source::setFriendlyName( const QString& fname )
- {
- Q_D( Source );
- if ( fname.isEmpty() )
- {
- return;
- }
- d->friendlyname = fname;
- }
- QString
- Source::dbFriendlyName() const
- {
- Q_D( const Source );
- if ( d->dbFriendlyName.isEmpty() )
- {
- return nodeId();
- }
- return d->dbFriendlyName;
- }
- void
- Source::setDbFriendlyName( const QString& dbFriendlyName )
- {
- Q_D( Source );
- if ( dbFriendlyName.isEmpty() )
- return;
- d->dbFriendlyName = dbFriendlyName;
- }
- void
- Source::addCollection( const collection_ptr& c )
- {
- Q_D( Source );
- //Q_ASSERT( m_collections.isEmpty() ); // only 1 source supported atm
- d->collections.append( c );
- emit collectionAdded( c );
- }
- void
- Source::removeCollection( const collection_ptr& c )
- {
- Q_D( Source );
- //Q_ASSERT( m_collections.length() == 1 && m_collections.first() == c ); // only 1 source supported atm
- d->collections.removeAll( c );
- emit collectionRemoved( c );
- }
- int
- Source::id() const
- {
- Q_D( const Source );
- return d->id;
- }
- ControlConnection*
- Source::controlConnection() const
- {
- Q_D( const Source );
- return d->cc.data();
- }
- void
- Source::handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason reason )
- {
- if ( reason == Tomahawk::Accounts::AccountManager::Disabled )
- setOffline();
- }
- void
- Source::setOffline()
- {
- Q_D( Source );
- qDebug() << Q_FUNC_INFO << friendlyName();
- if ( !d->online )
- return;
- d->online = false;
- emit offline();
- if ( !isLocal() )
- {
- d->currentTrack.clear();
- emit stateChanged();
- d->cc = 0;
- DatabaseCommand_SourceOffline* cmd = new DatabaseCommand_SourceOffline( id() );
- Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
- }
- }
- void
- Source::setOnline( bool force )
- {
- Q_D( Source );
- tDebug( LOGVERBOSE ) << Q_FUNC_INFO << friendlyName();
- if ( d->online && !force )
- return;
- d->online = true;
- emit online();
- if ( !isLocal() )
- {
- // ensure username is in the database
- DatabaseCommand_addSource* cmd = new DatabaseCommand_addSource( d->nodeId, dbFriendlyName() );
- connect( cmd, SIGNAL( done( unsigned int, QString ) ),
- SLOT( dbLoaded( unsigned int, const QString& ) ) );
- Database::instance()->enqueue( Tomahawk::dbcmd_ptr(cmd) );
- }
- }
- void
- Source::dbLoaded( unsigned int id, const QString& fname )
- {
- Q_D( Source );
- d->id = id;
- setDbFriendlyName( fname );
- emit syncedWithDatabase();
- }
- void
- Source::scanningFinished( bool updateGUI )
- {
- Q_D( Source );
- d->textStatus = QString();
- if ( d->updateIndexWhenSynced )
- {
- d->updateIndexWhenSynced = false;
- updateTracks();
- }
- emit stateChanged();
- if ( updateGUI )
- emit synced();
- }
- void
- Source::onStateChanged( Tomahawk::DBSyncConnectionState newstate, Tomahawk::DBSyncConnectionState oldstate, const QString& info )
- {
- Q_D( Source );
- Q_UNUSED( oldstate );
- QString msg;
- switch( newstate )
- {
- case CHECKING:
- {
- msg = tr( "Checking" );
- break;
- }
- case FETCHING:
- {
- msg = tr( "Syncing" );
- break;
- }
- case PARSING:
- {
- msg = tr( "Importing" );
- break;
- }
- case SCANNING:
- {
- msg = tr( "Scanning (%L1 tracks)" ).arg( info );
- break;
- }
- case SYNCED:
- {
- msg = QString();
- break;
- }
- default:
- msg = QString();
- }
- d->state = newstate;
- d->textStatus = msg;
- emit stateChanged();
- }
- unsigned int
- Source::trackCount() const
- {
- Q_D( const Source );
- return d->stats.value( "numfiles", 0 ).toUInt();
- }
- query_ptr
- Source::currentTrack() const
- {
- Q_D( const Source );
- return d->currentTrack;
- }
- Tomahawk::playlistinterface_ptr
- Source::playlistInterface()
- {
- Q_D( Source );
- if ( d->playlistInterface.isNull() )
- {
- Tomahawk::source_ptr source = SourceList::instance()->get( id() );
- d->playlistInterface = Tomahawk::playlistinterface_ptr( new Tomahawk::SourcePlaylistInterface( source.data() ) );
- }
- return d->playlistInterface;
- }
- QSharedPointer<QMutexLocker>
- Source::acquireLock()
- {
- Q_D( Source );
- return QSharedPointer<QMutexLocker>( new QMutexLocker( &d->mutex ) );
- }
- void
- Source::onPlaybackStarted( const Tomahawk::track_ptr& track, unsigned int duration )
- {
- Q_D( Source );
- tLog( LOGVERBOSE ) << Q_FUNC_INFO << track->toString();
- d->currentTrack = track->toQuery();
- d->currentTrackTimer.start( duration * 1000 + 900000 ); // duration comes in seconds
- if ( d->playlistInterface.isNull() )
- playlistInterface();
- emit playbackStarted( track );
- emit stateChanged();
- }
- void
- Source::onPlaybackFinished( const Tomahawk::track_ptr& track, const Tomahawk::PlaybackLog& log )
- {
- Q_D( Source );
- tDebug( LOGVERBOSE ) << Q_FUNC_INFO << track->toString();
- emit playbackFinished( track, log );
- d->currentTrack.clear();
- emit stateChanged();
- }
- void
- Source::trackTimerFired()
- {
- Q_D( Source );
- d->currentTrack.clear();
- emit stateChanged();
- }
- QString
- Source::lastCmdGuid() const
- {
- Q_D( const Source );
- QMutexLocker lock( &d->cmdMutex );
- return d->lastCmdGuid;
- }
- void
- Source::setLastCmdGuid( const QString& guid )
- {
- Q_D( Source );
- tLog( LOGVERBOSE ) << Q_FUNC_INFO << "name is" << friendlyName() << "and guid is" << guid;
- QMutexLocker lock( &d->cmdMutex );
- d->lastCmdGuid = guid;
- }
- void
- Source::addCommand( const dbcmd_ptr& command )
- {
- Q_D( Source );
- QMutexLocker lock( &d->cmdMutex );
- d->cmds << command;
- if ( !command->singletonCmd() )
- {
- d->lastCmdGuid = command->guid();
- }
- d->commandCount = d->cmds.count();
- }
- void
- Source::executeCommands()
- {
- Q_D( Source );
- if ( QThread::currentThread() != thread() )
- {
- QMetaObject::invokeMethod( this, "executeCommands", Qt::QueuedConnection );
- return;
- }
- bool commandsAvail = false;
- {
- QMutexLocker lock( &d->cmdMutex );
- commandsAvail = !d->cmds.isEmpty();
- }
- if ( commandsAvail )
- {
- QMutexLocker lock( &d->cmdMutex );
- QList< Tomahawk::dbcmd_ptr > cmdGroup;
- Tomahawk::dbcmd_ptr cmd = d->cmds.takeFirst();
- while ( cmd->groupable() )
- {
- cmdGroup << cmd;
- if ( !d->cmds.isEmpty() && d->cmds.first()->groupable() && d->cmds.first()->commandname() == cmd->commandname() )
- cmd = d->cmds.takeFirst();
- else
- break;
- }
- // return here when the last command finished
- connect( cmd.data(), SIGNAL( finished() ), SLOT( executeCommands() ) );
- if ( !cmdGroup.isEmpty() )
- {
- Database::instance()->enqueue( cmdGroup );
- }
- else
- {
- Database::instance()->enqueue( cmd );
- }
- int percentage = ( float( d->commandCount - d->cmds.count() ) / (float)d->commandCount ) * 100.0;
- d->textStatus = tr( "Saving (%1%)" ).arg( percentage );
- emit stateChanged();
- }
- else
- {
- if ( d->updateIndexWhenSynced )
- {
- d->updateIndexWhenSynced = false;
- updateTracks();
- }
- d->textStatus = QString();
- d->state = SYNCED;
- emit commandsFinished();
- emit stateChanged();
- emit synced();
- }
- }
- void
- Source::reportSocialAttributesChanged( DatabaseCommand_SocialAction* action )
- {
- Q_ASSERT( action );
- emit socialAttributesChanged( action->action() );
- if ( action->action() == "latchOn" )
- {
- const source_ptr to = SourceList::instance()->get( action->comment() );
- if ( !to.isNull() )
- emit latchedOn( to );
- }
- else if ( action->action() == "latchOff" )
- {
- const source_ptr from = SourceList::instance()->get( action->comment() );
- if ( !from.isNull() )
- emit latchedOff( from );
- }
- }
- void
- Source::updateTracks()
- {
- {
- DatabaseCommand* cmd = new DatabaseCommand_UpdateSearchIndex();
- Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
- }
- {
- // Re-calculate local db stats
- DatabaseCommand_CollectionStats* cmd = new DatabaseCommand_CollectionStats( SourceList::instance()->get( id() ) );
- connect( cmd, SIGNAL( done( QVariantMap ) ), SLOT( setStats( QVariantMap ) ), Qt::QueuedConnection );
- Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
- }
- }
- void
- Source::updateIndexWhenSynced()
- {
- Q_D( Source );
- d->updateIndexWhenSynced = true;
- }
- QString
- Source::textStatus() const
- {
- Q_D( const Source );
- if ( !d->textStatus.isEmpty() )
- {
- return d->textStatus;
- }
- if ( !currentTrack().isNull() )
- {
- return currentTrack()->queryTrack()->track() + " - " + currentTrack()->queryTrack()->artist();
- }
- // do not use isOnline() here - it will always return true for the local source
- if ( d->online )
- {
- return tr( "Online" );
- }
- else
- {
- return tr( "Offline" );
- }
- }
- DBSyncConnectionState
- Source::state() const
- {
- Q_D( const Source );
- return d->state;
- }