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 }