PageRenderTime 348ms CodeModel.GetById 61ms app.highlight 196ms RepoModel.GetById 36ms app.codeStats 2ms

/src/libtomahawk/Source.cpp

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