1 /****************************************************************************************
2 * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz> *
3 * *
4 * This program is free software; you can redistribute it and/or modify it under *
5 * the terms of the GNU General Public License as published by the Free Software *
6 * Foundation; either version 2 of the License, or (at your option) any later *
7 * version. *
8 * *
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
11 * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
12 * *
13 * You should have received a copy of the GNU General Public License along with *
14 * this program. If not, see <http://www.gnu.org/licenses/>. *
15 ****************************************************************************************/
16
17 #include "Controller.h"
18
19 #include "EngineController.h"
20 #include "MainWindow.h"
21 #include "ProviderFactory.h"
22 #include "amarokconfig.h"
23 #include "core/collections/Collection.h"
24 #include "core/logger/Logger.h"
25 #include "core/meta/Meta.h"
26 #include "core/support/Amarok.h"
27 #include "core/support/Components.h"
28 #include "core/support/Debug.h"
29 #include "statsyncing/Config.h"
30 #include "statsyncing/Process.h"
31 #include "statsyncing/ScrobblingService.h"
32 #include "statsyncing/collection/CollectionProvider.h"
33 #include "statsyncing/ui/CreateProviderDialog.h"
34 #include "statsyncing/ui/ConfigureProviderDialog.h"
35
36 #include "MetaValues.h"
37
38 #include <KMessageBox>
39
40 #include <QTimer>
41
42 using namespace StatSyncing;
43
44 const int Controller::s_syncingTriggerTimeout( 5000 );
45
Controller(QObject * parent)46 Controller::Controller( QObject* parent )
47 : QObject( parent )
48 , m_startSyncingTimer( new QTimer( this ) )
49 , m_config( new Config( this ) )
50 , m_updateNowPlayingTimer( new QTimer( this ) )
51 {
52 qRegisterMetaType<ScrobblingServicePtr>();
53
54 m_startSyncingTimer->setSingleShot( true );
55 connect( m_startSyncingTimer, &QTimer::timeout, this, &Controller::startNonInteractiveSynchronization );
56 CollectionManager *manager = CollectionManager::instance();
57 Q_ASSERT( manager );
58 connect( manager, &CollectionManager::collectionAdded, this, &Controller::slotCollectionAdded );
59 connect( manager, &CollectionManager::collectionRemoved, this, &Controller::slotCollectionRemoved );
60 delayedStartSynchronization();
61
62 EngineController *engine = Amarok::Components::engineController();
63 Q_ASSERT( engine );
64 connect( engine, &EngineController::trackFinishedPlaying,
65 this, &Controller::slotTrackFinishedPlaying );
66
67 m_updateNowPlayingTimer->setSingleShot( true );
68 m_updateNowPlayingTimer->setInterval( 10000 ); // wait 10s before updating
69 // We connect the signals to (re)starting the timer to postpone the submission a
70 // little to prevent frequent updates of rapidly - changing metadata
71 connect( engine, &EngineController::trackChanged,
72 m_updateNowPlayingTimer, QOverload<>::of(&QTimer::start) );
73 // following is needed for streams that don't Q_EMIT newTrackPlaying on song change
74 connect( engine, &EngineController::trackMetadataChanged,
75 m_updateNowPlayingTimer, QOverload<>::of(&QTimer::start) );
76 connect( m_updateNowPlayingTimer, &QTimer::timeout,
77 this, &Controller::slotUpdateNowPlayingWithCurrentTrack );
78 // we need to reset m_lastSubmittedNowPlayingTrack when a track is played twice
79 connect( engine, &EngineController::trackPlaying,
80 this, &Controller::slotResetLastSubmittedNowPlayingTrack );
81 }
82
~Controller()83 Controller::~Controller()
84 {
85 }
86
87 QList<qint64>
availableFields()88 Controller::availableFields()
89 {
90 // when fields are changed, please update translations in MetadataConfig::MetadataConfig()
91 return QList<qint64>() << Meta::valRating << Meta::valFirstPlayed
92 << Meta::valLastPlayed << Meta::valPlaycount << Meta::valLabel;
93 }
94
95 void
registerProvider(const ProviderPtr & provider)96 Controller::registerProvider( const ProviderPtr &provider )
97 {
98 QString id = provider->id();
99 bool enabled = false;
100 if( m_config->providerKnown( id ) )
101 enabled = m_config->providerEnabled( id, false );
102 else
103 {
104 switch( provider->defaultPreference() )
105 {
106 case Provider::Never:
107 case Provider::NoByDefault:
108 enabled = false;
109 break;
110 case Provider::Ask:
111 {
112 QString text = i18nc( "%1 is collection name", "%1 has an ability to "
113 "synchronize track meta-data such as play count or rating "
114 "with other collections. Do you want to keep %1 synchronized?\n\n"
115 "You can always change the decision in Amarok configuration.",
116 provider->prettyName() );
117 enabled = KMessageBox::questionYesNo( The::mainWindow(), text ) == KMessageBox::Yes;
118 break;
119 }
120 case Provider::YesByDefault:
121 enabled = true;
122 break;
123 }
124 }
125
126 // don't tell config about Never-by-default providers
127 if( provider->defaultPreference() != Provider::Never )
128 {
129 m_config->updateProvider( id, provider->prettyName(), provider->icon(), true, enabled );
130 m_config->save();
131 }
132 m_providers.append( provider );
133 connect( provider.data(), &StatSyncing::Provider::updated, this, &Controller::slotProviderUpdated );
134 if( enabled )
135 delayedStartSynchronization();
136 }
137
138 void
unregisterProvider(const ProviderPtr & provider)139 Controller::unregisterProvider( const ProviderPtr &provider )
140 {
141 disconnect( provider.data(), 0, this, 0 );
142 if( m_config->providerKnown( provider->id() ) )
143 {
144 m_config->updateProvider( provider->id(), provider->prettyName(),
145 provider->icon(), /* online */ false );
146 m_config->save();
147 }
148 m_providers.removeAll( provider );
149 }
150
151 void
setFactories(const QList<QSharedPointer<Plugins::PluginFactory>> & factories)152 Controller::setFactories( const QList<QSharedPointer<Plugins::PluginFactory> > &factories )
153 {
154 for( const auto &pFactory : factories )
155 {
156 auto factory = qobject_cast<ProviderFactory*>( pFactory );
157 if( !factory )
158 continue;
159
160 if( m_providerFactories.contains( factory->type() ) ) // we have it already
161 continue;
162
163 m_providerFactories.insert( factory->type(), factory );
164 }
165 }
166
167 bool
hasProviderFactories() const168 Controller::hasProviderFactories() const
169 {
170 return !m_providerFactories.isEmpty();
171 }
172
173 bool
providerIsConfigurable(const QString & id) const174 Controller::providerIsConfigurable( const QString &id ) const
175 {
176 ProviderPtr provider = findRegisteredProvider( id );
177 return provider ? provider->isConfigurable() : false;
178 }
179
180 QWidget*
providerConfigDialog(const QString & id) const181 Controller::providerConfigDialog( const QString &id ) const
182 {
183 ProviderPtr provider = findRegisteredProvider( id );
184 if( !provider || !provider->isConfigurable() )
185 return 0;
186
187 ConfigureProviderDialog *dialog
188 = new ConfigureProviderDialog( id, provider->configWidget(),
189 The::mainWindow() );
190
191 connect( dialog, &StatSyncing::ConfigureProviderDialog::providerConfigured,
192 this, &Controller::reconfigureProvider );
193 connect( dialog, &StatSyncing::ConfigureProviderDialog::finished,
194 dialog, &StatSyncing::ConfigureProviderDialog::deleteLater );
195
196 return dialog;
197 }
198
199 QWidget*
providerCreationDialog() const200 Controller::providerCreationDialog() const
201 {
202 CreateProviderDialog *dialog = new CreateProviderDialog( The::mainWindow() );
203 for( const auto &factory : m_providerFactories )
204 dialog->addProviderType( factory->type(), factory->prettyName(),
205 factory->icon(), factory->createConfigWidget() );
206
207 connect( dialog, &StatSyncing::CreateProviderDialog::providerConfigured,
208 this, &Controller::createProvider );
209 connect( dialog, &StatSyncing::CreateProviderDialog::finished,
210 dialog, &StatSyncing::CreateProviderDialog::deleteLater );
211
212 return dialog;
213 }
214
215 void
createProvider(const QString & type,const QVariantMap & config)216 Controller::createProvider( const QString &type, const QVariantMap &config )
217 {
218 Q_ASSERT( m_providerFactories.contains( type ) );
219 m_providerFactories[type]->createProvider( config );
220 }
221
222 void
reconfigureProvider(const QString & id,const QVariantMap & config)223 Controller::reconfigureProvider( const QString &id, const QVariantMap &config )
224 {
225 ProviderPtr provider = findRegisteredProvider( id );
226 if( provider )
227 provider->reconfigure( config );
228 }
229
230 void
registerScrobblingService(const ScrobblingServicePtr & service)231 Controller::registerScrobblingService( const ScrobblingServicePtr &service )
232 {
233 if( m_scrobblingServices.contains( service ) )
234 {
235 warning() << __PRETTY_FUNCTION__ << "scrobbling service" << service << "already registered";
236 return;
237 }
238 m_scrobblingServices << service;
239 }
240
241 void
unregisterScrobblingService(const ScrobblingServicePtr & service)242 Controller::unregisterScrobblingService( const ScrobblingServicePtr &service )
243 {
244 m_scrobblingServices.removeAll( service );
245 }
246
247 QList<ScrobblingServicePtr>
scrobblingServices() const248 Controller::scrobblingServices() const
249 {
250 return m_scrobblingServices;
251 }
252
253 Config *
config()254 Controller::config()
255 {
256 return m_config;
257 }
258
259 void
synchronize()260 Controller::synchronize()
261 {
262 synchronizeWithMode( Process::Interactive );
263 }
264
265 void
scrobble(const Meta::TrackPtr & track,double playedFraction,const QDateTime & time)266 Controller::scrobble( const Meta::TrackPtr &track, double playedFraction, const QDateTime &time )
267 {
268 foreach( ScrobblingServicePtr service, m_scrobblingServices )
269 {
270 ScrobblingService::ScrobbleError error = service->scrobble( track, playedFraction, time );
271 if( error == ScrobblingService::NoError )
272 Q_EMIT trackScrobbled( service, track );
273 else
274 Q_EMIT scrobbleFailed( service, track, error );
275 }
276 }
277
278 void
slotProviderUpdated()279 Controller::slotProviderUpdated()
280 {
281 QObject *updatedProvider = sender();
282 Q_ASSERT( updatedProvider );
283 foreach( const ProviderPtr &provider, m_providers )
284 {
285 if( provider.data() == updatedProvider )
286 {
287 m_config->updateProvider( provider->id(), provider->prettyName(),
288 provider->icon(), true );
289 m_config->save();
290 }
291 }
292 }
293
294 void
delayedStartSynchronization()295 Controller::delayedStartSynchronization()
296 {
297 if( m_startSyncingTimer->isActive() )
298 m_startSyncingTimer->start( s_syncingTriggerTimeout ); // reset the timeout
299 else
300 {
301 m_startSyncingTimer->start( s_syncingTriggerTimeout );
302 // we could as well connect to all m_providers updated signals, but this serves
303 // for now
304 CollectionManager *manager = CollectionManager::instance();
305 Q_ASSERT( manager );
306 connect( manager, &CollectionManager::collectionDataChanged,
307 this, &Controller::delayedStartSynchronization );
308 }
309 }
310
311 void
slotCollectionAdded(Collections::Collection * collection,CollectionManager::CollectionStatus status)312 Controller::slotCollectionAdded( Collections::Collection *collection,
313 CollectionManager::CollectionStatus status )
314 {
315 if( status != CollectionManager::CollectionEnabled )
316 return;
317 ProviderPtr provider( new CollectionProvider( collection ) );
318 registerProvider( provider );
319 }
320
321 void
slotCollectionRemoved(const QString & id)322 Controller::slotCollectionRemoved( const QString &id )
323 {
324 // here we depend on StatSyncing::CollectionProvider returning identical id
325 // as collection
326 ProviderPtr provider = findRegisteredProvider( id );
327 if( provider )
328 unregisterProvider( provider );
329 }
330
331 void
startNonInteractiveSynchronization()332 Controller::startNonInteractiveSynchronization()
333 {
334 CollectionManager *manager = CollectionManager::instance();
335 Q_ASSERT( manager );
336 disconnect( manager, &CollectionManager::collectionDataChanged,
337 this, &Controller::delayedStartSynchronization );
338 synchronizeWithMode( Process::NonInteractive );
339 }
340
synchronizeWithMode(int intMode)341 void Controller::synchronizeWithMode( int intMode )
342 {
343 Process::Mode mode = Process::Mode( intMode );
344 if( m_currentProcess )
345 {
346 if( mode == StatSyncing::Process::Interactive )
347 m_currentProcess->raise();
348 return;
349 }
350
351 // read saved config
352 qint64 fields = m_config->checkedFields();
353 if( mode == Process::NonInteractive && fields == 0 )
354 return; // nothing to do
355 ProviderPtrSet checkedProviders;
356 foreach( ProviderPtr provider, m_providers )
357 {
358 if( m_config->providerEnabled( provider->id(), false ) )
359 checkedProviders.insert( provider );
360 }
361
362 ProviderPtrList usedProviders;
363 switch( mode )
364 {
365 case Process::Interactive:
366 usedProviders = m_providers;
367 break;
368 case Process::NonInteractive:
369 usedProviders = checkedProviders.toList();
370 break;
371 }
372 if( usedProviders.isEmpty() )
373 return; // nothing to do
374 if( usedProviders.count() == 1 && usedProviders.first()->id() == QLatin1String("localCollection") )
375 {
376 if( mode == StatSyncing::Process::Interactive )
377 {
378 QString text = i18n( "You only seem to have the Local Collection. Statistics "
379 "synchronization only makes sense if there is more than one collection." );
380 Amarok::Logger::longMessage( text );
381 }
382 return;
383 }
384
385 m_currentProcess = new Process( m_providers, checkedProviders, fields, mode, this );
386 m_currentProcess->start();
387 }
388
389 void
slotTrackFinishedPlaying(const Meta::TrackPtr & track,double playedFraction)390 Controller::slotTrackFinishedPlaying( const Meta::TrackPtr &track, double playedFraction )
391 {
392 if( !AmarokConfig::submitPlayedSongs() )
393 return;
394 Q_ASSERT( track );
395 scrobble( track, playedFraction );
396 }
397
398 void
slotResetLastSubmittedNowPlayingTrack()399 Controller::slotResetLastSubmittedNowPlayingTrack()
400 {
401 m_lastSubmittedNowPlayingTrack = Meta::TrackPtr();
402 }
403
404 void
slotUpdateNowPlayingWithCurrentTrack()405 Controller::slotUpdateNowPlayingWithCurrentTrack()
406 {
407 EngineController *engine = Amarok::Components::engineController();
408 if( !engine )
409 return;
410
411 Meta::TrackPtr track = engine->currentTrack(); // null track is okay
412 if( tracksVirtuallyEqual( track, m_lastSubmittedNowPlayingTrack ) )
413 {
414 debug() << __PRETTY_FUNCTION__ << "this track already recently submitted, ignoring";
415 return;
416 }
417 foreach( ScrobblingServicePtr service, m_scrobblingServices )
418 {
419 service->updateNowPlaying( track );
420 }
421
422 m_lastSubmittedNowPlayingTrack = track;
423 }
424
425 ProviderPtr
findRegisteredProvider(const QString & id) const426 Controller::findRegisteredProvider( const QString &id ) const
427 {
428 foreach( const ProviderPtr &provider, m_providers )
429 if( provider->id() == id )
430 return provider;
431
432 return ProviderPtr();
433 }
434
435 bool
tracksVirtuallyEqual(const Meta::TrackPtr & first,const Meta::TrackPtr & second)436 Controller::tracksVirtuallyEqual( const Meta::TrackPtr &first, const Meta::TrackPtr &second )
437 {
438 if( !first && !second )
439 return true; // both null
440 if( !first || !second )
441 return false; // exactly one is null
442 const QString firstAlbum = first->album() ? first->album()->name() : QString();
443 const QString secondAlbum = second->album() ? second->album()->name() : QString();
444 const QString firstArtist = first->artist() ? first->artist()->name() : QString();
445 const QString secondArtist = second->artist() ? second->artist()->name() : QString();
446 return first->name() == second->name() &&
447 firstAlbum == secondAlbum &&
448 firstArtist == secondArtist;
449 }
450