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 }