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