File indexing completed on 2024-05-05 04:49:21

0001 /****************************************************************************************
0002  * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz>                                      *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #include "Controller.h"
0018 
0019 #include "EngineController.h"
0020 #include "MainWindow.h"
0021 #include "ProviderFactory.h"
0022 #include "amarokconfig.h"
0023 #include "core/collections/Collection.h"
0024 #include "core/logger/Logger.h"
0025 #include "core/meta/Meta.h"
0026 #include "core/support/Amarok.h"
0027 #include "core/support/Components.h"
0028 #include "core/support/Debug.h"
0029 #include "statsyncing/Config.h"
0030 #include "statsyncing/Process.h"
0031 #include "statsyncing/ScrobblingService.h"
0032 #include "statsyncing/collection/CollectionProvider.h"
0033 #include "statsyncing/ui/CreateProviderDialog.h"
0034 #include "statsyncing/ui/ConfigureProviderDialog.h"
0035 
0036 #include "MetaValues.h"
0037 
0038 #include <KMessageBox>
0039 
0040 #include <QTimer>
0041 
0042 using namespace StatSyncing;
0043 
0044 const int Controller::s_syncingTriggerTimeout( 5000 );
0045 
0046 Controller::Controller( QObject* parent )
0047     : QObject( parent )
0048     , m_startSyncingTimer( new QTimer( this ) )
0049     , m_config( new Config( this ) )
0050     , m_updateNowPlayingTimer( new QTimer( this ) )
0051 {
0052     qRegisterMetaType<ScrobblingServicePtr>();
0053 
0054     m_startSyncingTimer->setSingleShot( true );
0055     connect( m_startSyncingTimer, &QTimer::timeout, this, &Controller::startNonInteractiveSynchronization );
0056     CollectionManager *manager = CollectionManager::instance();
0057     Q_ASSERT( manager );
0058     connect( manager, &CollectionManager::collectionAdded, this, &Controller::slotCollectionAdded );
0059     connect( manager, &CollectionManager::collectionRemoved, this, &Controller::slotCollectionRemoved );
0060     delayedStartSynchronization();
0061 
0062     EngineController *engine = Amarok::Components::engineController();
0063     Q_ASSERT( engine );
0064     connect( engine, &EngineController::trackFinishedPlaying,
0065              this, &Controller::slotTrackFinishedPlaying );
0066 
0067     m_updateNowPlayingTimer->setSingleShot( true );
0068     m_updateNowPlayingTimer->setInterval( 10000 ); // wait 10s before updating
0069     // We connect the signals to (re)starting the timer to postpone the submission a
0070     // little to prevent frequent updates of rapidly - changing metadata
0071     connect( engine, &EngineController::trackChanged,
0072              m_updateNowPlayingTimer, QOverload<>::of(&QTimer::start) );
0073     // following is needed for streams that don't Q_EMIT newTrackPlaying on song change
0074     connect( engine, &EngineController::trackMetadataChanged,
0075              m_updateNowPlayingTimer, QOverload<>::of(&QTimer::start) );
0076     connect( m_updateNowPlayingTimer, &QTimer::timeout,
0077              this, &Controller::slotUpdateNowPlayingWithCurrentTrack );
0078     // we need to reset m_lastSubmittedNowPlayingTrack when a track is played twice
0079     connect( engine, &EngineController::trackPlaying,
0080              this, &Controller::slotResetLastSubmittedNowPlayingTrack );
0081 }
0082 
0083 Controller::~Controller()
0084 {
0085 }
0086 
0087 QList<qint64>
0088 Controller::availableFields()
0089 {
0090     // when fields are changed, please update translations in MetadataConfig::MetadataConfig()
0091     return QList<qint64>() << Meta::valRating << Meta::valFirstPlayed
0092             << Meta::valLastPlayed << Meta::valPlaycount << Meta::valLabel;
0093 }
0094 
0095 void
0096 Controller::registerProvider( const ProviderPtr &provider )
0097 {
0098     QString id = provider->id();
0099     bool enabled = false;
0100     if( m_config->providerKnown( id ) )
0101         enabled = m_config->providerEnabled( id, false );
0102     else
0103     {
0104         switch( provider->defaultPreference() )
0105         {
0106             case Provider::Never:
0107             case Provider::NoByDefault:
0108                 enabled = false;
0109                 break;
0110             case Provider::Ask:
0111             {
0112                 QString text = i18nc( "%1 is collection name", "%1 has an ability to "
0113                     "synchronize track meta-data such as play count or rating "
0114                     "with other collections. Do you want to keep %1 synchronized?\n\n"
0115                     "You can always change the decision in Amarok configuration.",
0116                     provider->prettyName() );
0117                 enabled = KMessageBox::questionYesNo( The::mainWindow(), text ) == KMessageBox::Yes;
0118                 break;
0119             }
0120             case Provider::YesByDefault:
0121                 enabled = true;
0122                 break;
0123         }
0124     }
0125 
0126     // don't tell config about Never-by-default providers
0127     if( provider->defaultPreference() != Provider::Never )
0128     {
0129         m_config->updateProvider( id, provider->prettyName(), provider->icon(), true, enabled );
0130         m_config->save();
0131     }
0132     m_providers.append( provider );
0133     connect( provider.data(), &StatSyncing::Provider::updated, this, &Controller::slotProviderUpdated );
0134     if( enabled )
0135         delayedStartSynchronization();
0136 }
0137 
0138 void
0139 Controller::unregisterProvider( const ProviderPtr &provider )
0140 {
0141     disconnect( provider.data(), nullptr, this, nullptr );
0142     if( m_config->providerKnown( provider->id() ) )
0143     {
0144         m_config->updateProvider( provider->id(), provider->prettyName(),
0145                                   provider->icon(), /* online */ false );
0146         m_config->save();
0147     }
0148     m_providers.removeAll( provider );
0149 }
0150 
0151 void
0152 Controller::setFactories( const QList<QSharedPointer<Plugins::PluginFactory> > &factories )
0153 {
0154     for( const auto &pFactory : factories )
0155     {
0156         auto factory = qobject_cast<ProviderFactory*>( pFactory );
0157         if( !factory )
0158             continue;
0159 
0160         if( m_providerFactories.contains( factory->type() ) ) // we have it already
0161             continue;
0162 
0163         m_providerFactories.insert( factory->type(), factory );
0164     }
0165 }
0166 
0167 bool
0168 Controller::hasProviderFactories() const
0169 {
0170     return !m_providerFactories.isEmpty();
0171 }
0172 
0173 bool
0174 Controller::providerIsConfigurable( const QString &id ) const
0175 {
0176     ProviderPtr provider = findRegisteredProvider( id );
0177     return provider ? provider->isConfigurable() : false;
0178 }
0179 
0180 QWidget*
0181 Controller::providerConfigDialog( const QString &id ) const
0182 {
0183     ProviderPtr provider = findRegisteredProvider( id );
0184     if( !provider || !provider->isConfigurable() )
0185         return nullptr;
0186 
0187     ConfigureProviderDialog *dialog
0188             = new ConfigureProviderDialog( id, provider->configWidget(),
0189                                            The::mainWindow() );
0190 
0191     connect( dialog, &StatSyncing::ConfigureProviderDialog::providerConfigured,
0192              this, &Controller::reconfigureProvider );
0193     connect( dialog, &StatSyncing::ConfigureProviderDialog::finished,
0194              dialog, &StatSyncing::ConfigureProviderDialog::deleteLater );
0195 
0196     return dialog;
0197 }
0198 
0199 QWidget*
0200 Controller::providerCreationDialog() const
0201 {
0202     CreateProviderDialog *dialog = new CreateProviderDialog( The::mainWindow() );
0203     for( const auto &factory : m_providerFactories )
0204         dialog->addProviderType( factory->type(), factory->prettyName(),
0205                                  factory->icon(), factory->createConfigWidget() );
0206 
0207     connect( dialog, &StatSyncing::CreateProviderDialog::providerConfigured,
0208              this, &Controller::createProvider );
0209     connect( dialog, &StatSyncing::CreateProviderDialog::finished,
0210              dialog, &StatSyncing::CreateProviderDialog::deleteLater );
0211 
0212     return dialog;
0213 }
0214 
0215 void
0216 Controller::createProvider( const QString &type, const QVariantMap &config )
0217 {
0218     Q_ASSERT( m_providerFactories.contains( type ) );
0219     m_providerFactories[type]->createProvider( config );
0220 }
0221 
0222 void
0223 Controller::reconfigureProvider( const QString &id, const QVariantMap &config )
0224 {
0225     ProviderPtr provider = findRegisteredProvider( id );
0226     if( provider )
0227         provider->reconfigure( config );
0228 }
0229 
0230 void
0231 Controller::registerScrobblingService( const ScrobblingServicePtr &service )
0232 {
0233     if( m_scrobblingServices.contains( service ) )
0234     {
0235         warning() << __PRETTY_FUNCTION__ << "scrobbling service" << service << "already registered";
0236         return;
0237     }
0238     m_scrobblingServices << service;
0239 }
0240 
0241 void
0242 Controller::unregisterScrobblingService( const ScrobblingServicePtr &service )
0243 {
0244     m_scrobblingServices.removeAll( service );
0245 }
0246 
0247 QList<ScrobblingServicePtr>
0248 Controller::scrobblingServices() const
0249 {
0250     return m_scrobblingServices;
0251 }
0252 
0253 Config *
0254 Controller::config()
0255 {
0256     return m_config;
0257 }
0258 
0259 void
0260 Controller::synchronize()
0261 {
0262     synchronizeWithMode( Process::Interactive );
0263 }
0264 
0265 void
0266 Controller::scrobble( const Meta::TrackPtr &track, double playedFraction, const QDateTime &time )
0267 {
0268     foreach( ScrobblingServicePtr service, m_scrobblingServices )
0269     {
0270         ScrobblingService::ScrobbleError error = service->scrobble( track, playedFraction, time );
0271         if( error == ScrobblingService::NoError )
0272             Q_EMIT trackScrobbled( service, track );
0273         else
0274             Q_EMIT scrobbleFailed( service, track, error );
0275     }
0276 }
0277 
0278 void
0279 Controller::slotProviderUpdated()
0280 {
0281     QObject *updatedProvider = sender();
0282     Q_ASSERT( updatedProvider );
0283     foreach( const ProviderPtr &provider, m_providers )
0284     {
0285         if( provider.data() == updatedProvider )
0286         {
0287             m_config->updateProvider( provider->id(), provider->prettyName(),
0288                                       provider->icon(), true );
0289             m_config->save();
0290         }
0291     }
0292 }
0293 
0294 void
0295 Controller::delayedStartSynchronization()
0296 {
0297     if( m_startSyncingTimer->isActive() )
0298         m_startSyncingTimer->start( s_syncingTriggerTimeout ); // reset the timeout
0299     else
0300     {
0301         m_startSyncingTimer->start( s_syncingTriggerTimeout );
0302         // we could as well connect to all m_providers updated signals, but this serves
0303         // for now
0304         CollectionManager *manager = CollectionManager::instance();
0305         Q_ASSERT( manager );
0306         connect( manager, &CollectionManager::collectionDataChanged,
0307                  this, &Controller::delayedStartSynchronization );
0308     }
0309 }
0310 
0311 void
0312 Controller::slotCollectionAdded( Collections::Collection *collection,
0313                                  CollectionManager::CollectionStatus status )
0314 {
0315     if( status != CollectionManager::CollectionEnabled )
0316         return;
0317     ProviderPtr provider( new CollectionProvider( collection ) );
0318     registerProvider( provider );
0319 }
0320 
0321 void
0322 Controller::slotCollectionRemoved( const QString &id )
0323 {
0324     // here we depend on StatSyncing::CollectionProvider returning identical id
0325     // as collection
0326     ProviderPtr provider = findRegisteredProvider( id );
0327     if( provider )
0328         unregisterProvider( provider );
0329 }
0330 
0331 void
0332 Controller::startNonInteractiveSynchronization()
0333 {
0334     CollectionManager *manager = CollectionManager::instance();
0335     Q_ASSERT( manager );
0336     disconnect( manager, &CollectionManager::collectionDataChanged,
0337                 this, &Controller::delayedStartSynchronization );
0338     synchronizeWithMode( Process::NonInteractive );
0339 }
0340 
0341 void Controller::synchronizeWithMode( int intMode )
0342 {
0343     Process::Mode mode = Process::Mode( intMode );
0344     if( m_currentProcess )
0345     {
0346         if( mode == StatSyncing::Process::Interactive )
0347             m_currentProcess->raise();
0348         return;
0349     }
0350 
0351     // read saved config
0352     qint64 fields = m_config->checkedFields();
0353     if( mode == Process::NonInteractive && fields == 0 )
0354         return; // nothing to do
0355     ProviderPtrSet checkedProviders;
0356     foreach( ProviderPtr provider, m_providers )
0357     {
0358         if( m_config->providerEnabled( provider->id(), false ) )
0359             checkedProviders.insert( provider );
0360     }
0361 
0362     ProviderPtrList usedProviders;
0363     switch( mode )
0364     {
0365         case Process::Interactive:
0366             usedProviders = m_providers;
0367             break;
0368         case Process::NonInteractive:
0369             usedProviders = checkedProviders.values();
0370             break;
0371     }
0372     if( usedProviders.isEmpty() )
0373         return; // nothing to do
0374     if( usedProviders.count() == 1 && usedProviders.first()->id() == QLatin1String("localCollection") )
0375     {
0376         if( mode == StatSyncing::Process::Interactive )
0377         {
0378             QString text = i18n( "You only seem to have the Local Collection. Statistics "
0379                 "synchronization only makes sense if there is more than one collection." );
0380             Amarok::Logger::longMessage( text );
0381         }
0382         return;
0383     }
0384 
0385     m_currentProcess = new Process( m_providers, checkedProviders, fields, mode, this );
0386     m_currentProcess->start();
0387 }
0388 
0389 void
0390 Controller::slotTrackFinishedPlaying( const Meta::TrackPtr &track, double playedFraction )
0391 {
0392     if( !AmarokConfig::submitPlayedSongs() )
0393         return;
0394     Q_ASSERT( track );
0395     scrobble( track, playedFraction );
0396 }
0397 
0398 void
0399 Controller::slotResetLastSubmittedNowPlayingTrack()
0400 {
0401     m_lastSubmittedNowPlayingTrack = Meta::TrackPtr();
0402 }
0403 
0404 void
0405 Controller::slotUpdateNowPlayingWithCurrentTrack()
0406 {
0407     EngineController *engine = Amarok::Components::engineController();
0408     if( !engine )
0409         return;
0410 
0411     Meta::TrackPtr track = engine->currentTrack(); // null track is okay
0412     if( tracksVirtuallyEqual( track, m_lastSubmittedNowPlayingTrack ) )
0413     {
0414         debug() << __PRETTY_FUNCTION__ << "this track already recently submitted, ignoring";
0415         return;
0416     }
0417     foreach( ScrobblingServicePtr service, m_scrobblingServices )
0418     {
0419         service->updateNowPlaying( track );
0420     }
0421 
0422     m_lastSubmittedNowPlayingTrack = track;
0423 }
0424 
0425 ProviderPtr
0426 Controller::findRegisteredProvider( const QString &id ) const
0427 {
0428     foreach( const ProviderPtr &provider, m_providers )
0429         if( provider->id() == id )
0430             return provider;
0431 
0432     return ProviderPtr();
0433 }
0434 
0435 bool
0436 Controller::tracksVirtuallyEqual( const Meta::TrackPtr &first, const Meta::TrackPtr &second )
0437 {
0438     if( !first && !second )
0439         return true; // both null
0440     if( !first || !second )
0441         return false; // exactly one is null
0442     const QString firstAlbum = first->album() ? first->album()->name() : QString();
0443     const QString secondAlbum = second->album() ? second->album()->name() : QString();
0444     const QString firstArtist = first->artist() ? first->artist()->name() : QString();
0445     const QString secondArtist = second->artist() ? second->artist()->name() : QString();
0446     return first->name() == second->name() &&
0447            firstAlbum == secondAlbum &&
0448            firstArtist == secondArtist;
0449 }