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 "Process.h"
0018 
0019 #include "MainWindow.h"
0020 #include "MetaValues.h"
0021 #include "core/logger/Logger.h"
0022 #include "core/support/Amarok.h"
0023 #include "core/support/Debug.h"
0024 #include "core/support/Components.h"
0025 #include "statsyncing/Config.h"
0026 #include "statsyncing/Controller.h"
0027 #include "statsyncing/jobs/MatchTracksJob.h"
0028 #include "statsyncing/jobs/SynchronizeTracksJob.h"
0029 #include "statsyncing/models/MatchedTracksModel.h"
0030 #include "statsyncing/models/ProvidersModel.h"
0031 #include "statsyncing/models/SingleTracksModel.h"
0032 #include "statsyncing/ui/ChooseProvidersPage.h"
0033 #include "statsyncing/ui/MatchedTracksPage.h"
0034 
0035 #include <KWindowConfig>
0036 
0037 using namespace StatSyncing;
0038 
0039 Process::Process( const ProviderPtrList &providers, const ProviderPtrSet &preSelectedProviders,
0040                   qint64 checkedFields, Process::Mode mode, QObject *parent )
0041     : QObject( parent )
0042     , m_mode( mode )
0043     , m_providersModel( new ProvidersModel( providers, preSelectedProviders, this ) )
0044     , m_checkedFields( checkedFields )
0045     , m_matchedTracksModel( nullptr )
0046     , m_dialog( new QDialog() )
0047 {
0048     m_dialog->setWindowTitle( i18n( "Synchronize Statistics" ) );
0049     m_dialog->resize( QSize( 860, 500 ) );
0050     KWindowConfig::restoreWindowSize( m_dialog->windowHandle(), Amarok::config( QStringLiteral("StatSyncingDialog") ) );
0051 
0052     // delete this process when user hits the close button
0053     connect( m_dialog.data(), &QDialog::finished, this, &Process::slotSaveSizeAndDelete );
0054 
0055     /* we need to delete all QWidgets on application exit well before QApplication
0056      * is destroyed. We however don't set MainWindow as parent as this would make
0057      * StatSyncing dialog share taskbar entry with Amarok main window */
0058     connect( The::mainWindow(), &MainWindow::destroyed, this, &Process::slotDeleteDialog );
0059 }
0060 
0061 Process::~Process()
0062 {
0063     delete m_dialog.data(); // we cannot deleteLater, dialog references m_matchedTracksModel
0064 }
0065 
0066 void
0067 Process::start()
0068 {
0069     if( m_mode == Interactive )
0070     {
0071         m_providersPage = new ChooseProvidersPage();
0072         m_providersPage->setFields( Controller::availableFields(), m_checkedFields );
0073         m_providersPage->setProvidersModel( m_providersModel, m_providersModel->selectionModel() );
0074 
0075         connect( m_providersPage.data(), &StatSyncing::ChooseProvidersPage::accepted,
0076                  this, &Process::slotMatchTracks );
0077         connect( m_providersPage.data(), &StatSyncing::ChooseProvidersPage::rejected,
0078                  this, &Process::slotSaveSizeAndDelete );
0079 
0080         for( const auto &child : m_dialog->children() )
0081         {
0082             auto widget = qobject_cast<QWidget*>( child );
0083             if( widget )
0084             {
0085                 widget->hide(); // otherwise it may last as a ghost image
0086                 widget->deleteLater();
0087             }
0088         }
0089         m_providersPage->setParent( m_dialog ); // takes ownership
0090         raise();
0091     }
0092     else if( m_checkedFields )
0093         slotMatchTracks();
0094 }
0095 
0096 void
0097 Process::raise()
0098 {
0099     if( m_providersPage || m_tracksPage )
0100     {
0101         m_dialog->show();
0102         m_dialog->activateWindow();
0103         m_dialog->raise();
0104     }
0105     else
0106         m_mode = Interactive; // schedule dialog should be shown when something happens
0107 }
0108 
0109 void
0110 Process::slotMatchTracks()
0111 {
0112     MatchTracksJob *job = new MatchTracksJob( m_providersModel->selectedProviders() );
0113     QString text = i18n( "Matching Tracks for Statistics Synchronization" );
0114     if( m_providersPage )
0115     {
0116         ChooseProvidersPage *page = m_providersPage.data(); // too lazy to type
0117         m_checkedFields = page->checkedFields();
0118 
0119         page->disableControls();
0120         page->setProgressBarText( text );
0121         connect( job, &StatSyncing::MatchTracksJob::totalSteps,
0122                  page, &StatSyncing::ChooseProvidersPage::setProgressBarMaximum );
0123         connect( job, &StatSyncing::MatchTracksJob::incrementProgress, page,
0124                  &StatSyncing::ChooseProvidersPage::progressBarIncrementProgress );
0125         connect( page, &StatSyncing::ChooseProvidersPage::rejected, job, &StatSyncing::MatchTracksJob::abort );
0126         connect( m_dialog.data(), &QDialog::finished, job, &StatSyncing::MatchTracksJob::abort );
0127     }
0128     else // background operation
0129     {
0130         Amarok::Logger::newProgressOperation( job, text, 100, job, &MatchTracksJob::abort );
0131     }
0132 
0133     connect( job, &StatSyncing::MatchTracksJob::done, this, &Process::slotTracksMatched );
0134     connect( job, &StatSyncing::MatchTracksJob::done, job, &StatSyncing::MatchTracksJob::deleteLater );
0135     ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
0136 }
0137 
0138 void
0139 Process::slotTracksMatched( ThreadWeaver::JobPointer job )
0140 {
0141     MatchTracksJob *matchJob = dynamic_cast<MatchTracksJob *>( job.data() );
0142     if( !matchJob )
0143     {
0144         error() << __PRETTY_FUNCTION__ << "Failed cast, should never happen";
0145         deleteLater();
0146         return;
0147     }
0148     if( !matchJob->success() )
0149     {
0150         warning() << __PRETTY_FUNCTION__ << "MatchTracksJob failed";
0151         deleteLater();
0152         return;
0153     }
0154     StatSyncing::Controller *controller = Amarok::Components::statSyncingController();
0155     if( !controller )
0156     {
0157         warning() << __PRETTY_FUNCTION__ << "StatSyncing::Controller disappeared";
0158         deleteLater();
0159         return;
0160     }
0161 
0162     // remove fields that are not writable:
0163     qint64 usedFields = m_checkedFields & m_providersModel->writableTrackStatsDataUnion();
0164     m_options.setSyncedFields( usedFields );
0165     m_options.setExcludedLabels( controller->config()->excludedLabels() );
0166     QList<qint64> columns = QList<qint64>() << Meta::valTitle;
0167     foreach( qint64 field, Controller::availableFields() )
0168     {
0169         if( field & usedFields )
0170             columns << field;
0171     }
0172 
0173     m_matchedTracksModel = new MatchedTracksModel( matchJob->matchedTuples(), columns,
0174                                                    m_options, this );
0175     QList<ScrobblingServicePtr> services = controller->scrobblingServices();
0176     // only fill in m_tracksToScrobble if there are actual scrobbling services available
0177     m_tracksToScrobble = services.isEmpty() ? TrackList() : matchJob->tracksToScrobble();
0178 
0179     if( m_matchedTracksModel->hasConflict() || m_mode == Interactive )
0180     {
0181         m_tracksPage = new MatchedTracksPage();
0182         m_tracksPage->setProviders( matchJob->providers() );
0183         m_tracksPage->setMatchedTracksModel( m_matchedTracksModel );
0184         foreach( ProviderPtr provider, matchJob->providers() )
0185         {
0186             if( !matchJob->uniqueTracks().value( provider ).isEmpty() )
0187                 m_tracksPage->addUniqueTracksModel( provider, new SingleTracksModel(
0188                     matchJob->uniqueTracks().value( provider ), columns, m_options, m_tracksPage ) );
0189             if( !matchJob->excludedTracks().value( provider ).isEmpty() )
0190                 m_tracksPage->addExcludedTracksModel( provider, new SingleTracksModel(
0191                     matchJob->excludedTracks().value( provider ), columns, m_options, m_tracksPage ) );
0192         }
0193         m_tracksPage->setTracksToScrobble( m_tracksToScrobble, services );
0194 
0195         connect( m_tracksPage, &StatSyncing::MatchedTracksPage::back, this, &Process::slotBack );
0196         connect( m_tracksPage, &StatSyncing::MatchedTracksPage::accepted, this, &Process::slotSynchronize );
0197         connect( m_tracksPage, &StatSyncing::MatchedTracksPage::rejected, this, &Process::slotSaveSizeAndDelete );
0198         for( const auto &child : m_dialog->children() )
0199         {
0200             auto widget = qobject_cast<QWidget*>( child );
0201             if( widget )
0202             {
0203                 widget->hide(); // otherwise it may last as a ghost image
0204                 widget->deleteLater();
0205             }
0206         }
0207         m_tracksPage->setParent( m_dialog ); // takes ownership
0208         raise();
0209     }
0210     else // NonInteractive mode without conflict
0211         slotSynchronize();
0212 }
0213 
0214 void
0215 Process::slotBack()
0216 {
0217     m_mode = Interactive; // reset mode, we're interactive from this point
0218     start();
0219 }
0220 
0221 void
0222 Process::slotSynchronize()
0223 {
0224     // disconnect, otherwise we prematurely delete Process and thus m_matchedTracksModel
0225     disconnect( m_dialog.data(), &QDialog::finished, this, &Process::slotSaveSizeAndDelete );
0226     m_dialog.data()->close();
0227 
0228     SynchronizeTracksJob *job = new SynchronizeTracksJob(
0229             m_matchedTracksModel->matchedTuples(), m_tracksToScrobble, m_options );
0230     QString text = i18n( "Synchronizing Track Statistics" );
0231     Amarok::Logger::newProgressOperation( job, text, 100, job, &SynchronizeTracksJob::abort );
0232     connect( job, &StatSyncing::SynchronizeTracksJob::done, this, &Process::slotLogSynchronization );
0233     connect( job, &StatSyncing::SynchronizeTracksJob::done, job, &StatSyncing::SynchronizeTracksJob::deleteLater );
0234     ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
0235 }
0236 
0237 void
0238 Process::slotLogSynchronization( ThreadWeaver::JobPointer job )
0239 {
0240     deleteLater(); // our work is done
0241     SynchronizeTracksJob *syncJob = dynamic_cast<SynchronizeTracksJob *>( job.data() );
0242     if( !syncJob )
0243     {
0244         warning() << __PRETTY_FUNCTION__ << "syncJob is null";
0245         return;
0246     }
0247 
0248     int updatedTracksCount = syncJob->updatedTracksCount();
0249     QMap<ScrobblingServicePtr, QMap<ScrobblingService::ScrobbleError, int> > scrobbles =
0250             syncJob->scrobbles();
0251 
0252     QStringList providerNames;
0253     foreach( ProviderPtr provider, m_providersModel->selectedProviders() )
0254         providerNames << "<b>" + provider->prettyName() + "</b>";
0255     QString providers = providerNames.join( i18nc( "comma between list words", ", " ) );
0256 
0257     QStringList text = QStringList() << i18ncp( "%2 is a list of collection names",
0258             "Synchronization of %2 done. <b>One</b> track was updated.",
0259             "Synchronization of %2 done. <b>%1</b> tracks were updated.",
0260             updatedTracksCount, providers );
0261 
0262     QMap<ScrobblingService::ScrobbleError, int> scrobbleErrorCounts;
0263     foreach( const ScrobblingServicePtr &provider, scrobbles.keys() )
0264     {
0265         QString name = "<b>" + provider->prettyName() + "</b>";
0266         QMap<ScrobblingService::ScrobbleError, int> &providerScrobbles = scrobbles[ provider ];
0267 
0268         QMapIterator<ScrobblingService::ScrobbleError, int> it( providerScrobbles );
0269         while( it.hasNext() )
0270         {
0271             it.next();
0272             if( it.key() == ScrobblingService::NoError )
0273                 text << i18np( "<b>One</b> track was queued for scrobbling to %2.",
0274                         "<b>%1</b> tracks were queued for scrobbling to %2.", it.value(), name );
0275             else
0276                 scrobbleErrorCounts[ it.key() ] += it.value();
0277         }
0278     }
0279     if( scrobbleErrorCounts.value( ScrobblingService::TooShort ) )
0280         text << i18np( "<b>One</b> track's played time was too short to be scrobbled.",
0281                        "<b>%1</b> tracks' played time was too short to be scrobbled.",
0282                        scrobbleErrorCounts[ ScrobblingService::TooShort ] );
0283     if( scrobbleErrorCounts.value( ScrobblingService::BadMetadata ) )
0284         text << i18np( "<b>One</b> track had insufficient metadata to be scrobbled.",
0285                        "<b>%1</b> tracks had insufficient metadata to be scrobbled.",
0286                        scrobbleErrorCounts[ ScrobblingService::BadMetadata ] );
0287     if( scrobbleErrorCounts.value( ScrobblingService::FromTheFuture ) )
0288         text << i18np( "<b>One</b> track was reported to have been played in the future.",
0289                        "<b>%1</b> tracks were reported to have been played in the future.",
0290                        scrobbleErrorCounts[ ScrobblingService::FromTheFuture ] );
0291     if( scrobbleErrorCounts.value( ScrobblingService::FromTheDistantPast ) )
0292         text << i18np( "<b>One</b> track was last played in too distant past to be scrobbled.",
0293                        "<b>%1</b> tracks were last played in too distant past to be scrobbled.",
0294                        scrobbleErrorCounts[ ScrobblingService::FromTheDistantPast ] );
0295     if( scrobbleErrorCounts.value( ScrobblingService::SkippedByUser ) )
0296         text << i18np( "Scrobbling of <b>one</b> track was skipped as configured by the user.",
0297                        "Scrobbling of <b>%1</b> tracks was skipped as configured by the user.",
0298                        scrobbleErrorCounts[ ScrobblingService::SkippedByUser ] );
0299 
0300     Amarok::Logger::longMessage( text.join( QStringLiteral("<br>\n") ) );
0301 }
0302 
0303 void
0304 Process::slotSaveSizeAndDelete()
0305 {
0306     if( m_dialog )
0307     {
0308         KConfigGroup group = Amarok::config( QStringLiteral("StatSyncingDialog") );
0309         group.writeEntry( "geometry", m_dialog->saveGeometry() );
0310     }
0311     deleteLater();
0312 }
0313 
0314 void
0315 Process::slotDeleteDialog()
0316 {
0317     // we cannot use deleteLater(), we don't have spare eventloop iteration
0318     delete m_dialog.data();
0319 }