File indexing completed on 2024-05-19 04:50:15
0001 /**************************************************************************************** 0002 * Copyright (c) 2007 Shane King <kde@dontletsstart.com> * 0003 * Copyright (c) 2008 Leo Franchi <lfranchi@kde.org> * 0004 * Copyright (c) 2009 Casey Link <unnamedrambler@gmail.com> * 0005 * * 0006 * This program is free software; you can redistribute it and/or modify it under * 0007 * the terms of the GNU General Public License as published by the Free Software * 0008 * Foundation; either version 2 of the License, or (at your option) any later * 0009 * version. * 0010 * * 0011 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0012 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0013 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0014 * * 0015 * You should have received a copy of the GNU General Public License along with * 0016 * this program. If not, see <http://www.gnu.org/licenses/>. * 0017 ****************************************************************************************/ 0018 0019 #define DEBUG_PREFIX "LastFmService" 0020 #include "core/support/Debug.h" 0021 0022 #include "LastFmService.h" 0023 0024 #include "AvatarDownloader.h" 0025 #include "EngineController.h" 0026 #include "biases/LastFmBias.h" 0027 #include "biases/WeeklyTopBias.h" 0028 #include "LastFmServiceCollection.h" 0029 #include "LastFmServiceConfig.h" 0030 #include "LoveTrackAction.h" 0031 #include "SimilarArtistsAction.h" 0032 #include "LastFmTreeModel.h" 0033 #include "LastFmTreeView.h" 0034 #include "ScrobblerAdapter.h" 0035 #include "GlobalCurrentTrackActions.h" 0036 #include "core/support/Components.h" 0037 #include "core/logger/Logger.h" 0038 #include "meta/LastFmMeta.h" 0039 #include "SynchronizationAdapter.h" 0040 #include "statsyncing/Controller.h" 0041 #include "widgets/SearchWidget.h" 0042 0043 #include <QLineEdit> 0044 #include <KPluginFactory> 0045 0046 #include <QCryptographicHash> 0047 #include <QGroupBox> 0048 #include <QHBoxLayout> 0049 #include <QLabel> 0050 #include <QPixmap> 0051 #include <QStandardPaths> 0052 #include <QTimer> 0053 0054 #include <XmlQuery.h> 0055 0056 0057 LastFmServiceFactory::LastFmServiceFactory() 0058 : ServiceFactory() 0059 {} 0060 0061 void 0062 LastFmServiceFactory::init() 0063 { 0064 ServiceBase *service = new LastFmService( this, "Last.fm" ); 0065 m_initialized = true; 0066 emit newService( service ); 0067 } 0068 0069 QString 0070 LastFmServiceFactory::name() 0071 { 0072 return "Last.fm"; 0073 } 0074 0075 KConfigGroup 0076 LastFmServiceFactory::config() 0077 { 0078 return Amarok::config( LastFmServiceConfig::configSectionName() ); 0079 } 0080 0081 bool 0082 LastFmServiceFactory::possiblyContainsTrack( const QUrl &url ) const 0083 { 0084 return url.scheme() == "lastfm"; 0085 } 0086 0087 0088 LastFmService::LastFmService( LastFmServiceFactory *parent, const QString &name ) 0089 : ServiceBase( name, parent, false ) 0090 , m_collection( nullptr ) 0091 , m_polished( false ) 0092 , m_avatarLabel( nullptr ) 0093 , m_profile( nullptr ) 0094 , m_userinfo( nullptr ) 0095 , m_subscriber( false ) 0096 , m_authenticateReply( nullptr ) 0097 , m_config( LastFmServiceConfig::instance() ) 0098 { 0099 DEBUG_BLOCK 0100 setShortDescription( i18n( "Last.fm: The social music revolution" ) ); 0101 setIcon( QIcon::fromTheme( "view-services-lastfm-amarok" ) ); 0102 setLongDescription( i18n( "Last.fm is a popular online service that provides personal radio stations and music recommendations. A personal listening station is tailored based on your listening habits and provides you with recommendations for new music. It is also possible to play stations with music that is similar to a particular artist as well as listen to streams from people you have added as friends" ) ); 0103 setImagePath( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/hover_info_lastfm.png" ) ); 0104 0105 //We have no use for searching currently.. 0106 m_searchWidget->setVisible( false ); 0107 0108 // set the global static Lastfm::Ws stuff 0109 lastfm::ws::ApiKey = Amarok::lastfmApiKey(); 0110 lastfm::ws::SharedSecret = Amarok::lastfmApiSharedSecret(); 0111 0112 // HTTPS is the only scheme supported by Auth 0113 lastfm::ws::setScheme(lastfm::ws::Https); 0114 0115 // set the nam TWICE. Yes. It prevents liblastfm from deleting it, see their code 0116 lastfm::setNetworkAccessManager( The::networkAccessManager() ); 0117 lastfm::setNetworkAccessManager( The::networkAccessManager() ); 0118 0119 // enable custom bias 0120 m_biasFactories << new Dynamic::LastFmBiasFactory(); 0121 Dynamic::BiasFactory::instance()->registerNewBiasFactory( m_biasFactories.last() ); 0122 m_biasFactories << new Dynamic::WeeklyTopBiasFactory(); 0123 Dynamic::BiasFactory::instance()->registerNewBiasFactory( m_biasFactories.last() ); 0124 0125 // add the "play similar artists" action to all artist 0126 The::globalCollectionActions()->addArtistAction( new SimilarArtistsAction( this ) ); 0127 The::globalCollectionActions()->addTrackAction( new LoveTrackAction( this ) ); 0128 0129 QAction *loveAction = new QAction( QIcon::fromTheme( "love-amarok" ), i18n( "Last.fm: Love" ), this ); 0130 connect( loveAction, &QAction::triggered, this, &LastFmService::loveCurrentTrack ); 0131 loveAction->setShortcut( i18n( "Ctrl+L" ) ); 0132 The::globalCurrentTrackActions()->addAction( loveAction ); 0133 0134 connect( m_config.data(), &LastFmServiceConfig::updated, this, &LastFmService::slotReconfigure ); 0135 QTimer::singleShot(0, this, &LastFmService::slotReconfigure); // call reconfigure but only after constructor is finished (because it might call virtual methods) 0136 } 0137 0138 LastFmService::~LastFmService() 0139 { 0140 DEBUG_BLOCK 0141 using namespace Dynamic; 0142 QMutableListIterator<AbstractBiasFactory *> it( m_biasFactories ); 0143 while( it.hasNext() ) 0144 { 0145 AbstractBiasFactory *factory = it.next(); 0146 it.remove(); 0147 0148 BiasFactory::instance()->removeBiasFactory( factory ); 0149 delete factory; 0150 } 0151 0152 if( m_collection ) 0153 { 0154 CollectionManager::instance()->removeTrackProvider( m_collection ); 0155 m_collection->deleteLater(); 0156 m_collection = nullptr; 0157 } 0158 0159 StatSyncing::Controller *controller = Amarok::Components::statSyncingController(); 0160 if( m_scrobbler && controller ) 0161 controller->unregisterScrobblingService( m_scrobbler.staticCast<StatSyncing::ScrobblingService>() ); 0162 if( m_synchronizationAdapter && controller ) 0163 controller->unregisterProvider( m_synchronizationAdapter ); 0164 } 0165 0166 void 0167 LastFmService::slotReconfigure() 0168 { 0169 lastfm::ws::Username = m_config->username(); 0170 bool ready = !m_config->username().isEmpty(); // core features require just username 0171 0172 /* create ServiceCollection only once the username is known (remember, getting 0173 * username from KWallet is async! */ 0174 if( !m_collection && ready ) 0175 { 0176 m_collection = new Collections::LastFmServiceCollection( m_config->username() ); 0177 CollectionManager::instance()->addTrackProvider( m_collection ); 0178 } 0179 0180 // create Model once the username is known, it depends on it implicitly 0181 if( !model() && ready ) 0182 { 0183 setModel( new LastFmTreeModel( this ) ); 0184 } 0185 0186 setServiceReady( ready ); // emits ready(), which needs to be done *after* creating collection 0187 0188 // now authenticate w/ last.fm and get our session key if we don't have one 0189 if( !m_config->sessionKey().isEmpty() ) 0190 { 0191 debug() << __PRETTY_FUNCTION__ << "using saved session key for last.fm"; 0192 continueReconfiguring(); 0193 } 0194 else if( !m_config->username().isEmpty() && !m_config->password().isEmpty() ) 0195 { 0196 debug() << __PRETTY_FUNCTION__ << "got no saved session key, authenticating with last.fm"; 0197 0198 // discard any possible ongoing auth connections 0199 if( m_authenticateReply ) 0200 { 0201 disconnect( m_authenticateReply, &QNetworkReply::finished, this, &LastFmService::onAuthenticated ); 0202 m_authenticateReply->abort(); 0203 m_authenticateReply->deleteLater(); 0204 m_authenticateReply = nullptr; 0205 } 0206 0207 QMap<QString, QString> query; 0208 query[ "method" ] = "auth.getMobileSession"; 0209 query[ "password" ] = m_config->password(); 0210 query[ "username" ] = m_config->username(); 0211 m_authenticateReply = lastfm::ws::post( query ); 0212 connect( m_authenticateReply, &QNetworkReply::finished, this, &LastFmService::onAuthenticated ); // calls continueReconfiguring() 0213 } 0214 else 0215 { 0216 debug() << __PRETTY_FUNCTION__ << "either last.fm username or password is empty"; 0217 continueReconfiguring(); 0218 } 0219 } 0220 0221 void 0222 LastFmService::continueReconfiguring() 0223 { 0224 StatSyncing::Controller *controller = Amarok::Components::statSyncingController(); 0225 Q_ASSERT( controller ); 0226 0227 lastfm::ws::SessionKey = m_config->sessionKey(); 0228 // we also check username, KWallet may deliver it really late, but we need it 0229 bool authenticated = serviceReady() && !m_config->sessionKey().isEmpty(); 0230 0231 if( m_scrobbler && (!authenticated || !m_config->scrobble()) ) 0232 { 0233 debug() << __PRETTY_FUNCTION__ << "unregistering and destroying ScrobblerAdapter"; 0234 controller->unregisterScrobblingService( m_scrobbler.staticCast<StatSyncing::ScrobblingService>() ); 0235 m_scrobbler.clear(); 0236 } 0237 else if( !m_scrobbler && authenticated && m_config->scrobble() ) 0238 { 0239 debug() << __PRETTY_FUNCTION__ << "creating and registering ScrobblerAdapter"; 0240 m_scrobbler = QSharedPointer<ScrobblerAdapter>( new ScrobblerAdapter( "Amarok", m_config ) ); 0241 controller->registerScrobblingService( m_scrobbler.staticCast<StatSyncing::ScrobblingService>() ); 0242 } 0243 0244 if( m_synchronizationAdapter && !authenticated ) 0245 { 0246 debug() << __PRETTY_FUNCTION__ << "unregistering and destroying SynchronizationAdapter"; 0247 controller->unregisterProvider( m_synchronizationAdapter ); 0248 m_synchronizationAdapter = nullptr; 0249 } 0250 else if( !m_synchronizationAdapter && authenticated ) 0251 { 0252 debug() << __PRETTY_FUNCTION__ << "creating and registering SynchronizationAdapter"; 0253 m_synchronizationAdapter = StatSyncing::ProviderPtr( new SynchronizationAdapter( m_config ) ); 0254 controller->registerProvider( m_synchronizationAdapter ); 0255 } 0256 0257 // update possibly changed user info 0258 QNetworkReply *reply = lastfm::User::getInfo(); 0259 connect( reply, &QNetworkReply::finished, this, &LastFmService::onGetUserInfo ); 0260 } 0261 0262 void 0263 LastFmService::onAuthenticated() 0264 { 0265 if( !m_authenticateReply ) 0266 warning() << __PRETTY_FUNCTION__ << "null reply!"; 0267 else 0268 m_authenticateReply->deleteLater(); 0269 0270 /* temporarily disconnect form config updates to prevent calling 0271 * slotReconfigure() for the second time. */ 0272 disconnect( m_config.data(), &LastFmServiceConfig::updated, this, &LastFmService::slotReconfigure ); 0273 0274 switch( m_authenticateReply ? m_authenticateReply->error() : QNetworkReply::UnknownNetworkError ) 0275 { 0276 case QNetworkReply::NoError: 0277 { 0278 lastfm::XmlQuery lfm; 0279 if( !lfm.parse( m_authenticateReply->readAll() ) || lfm.children( "error" ).size() > 0 ) 0280 { 0281 debug() << "error from authenticating with last.fm service:" << lfm.text(); 0282 m_config->setSessionKey( QString() ); 0283 m_config->save(); 0284 break; 0285 } 0286 m_config->setSessionKey( lfm[ "session" ][ "key" ].text() ); 0287 m_config->save(); 0288 0289 break; 0290 } 0291 case QNetworkReply::AuthenticationRequiredError: 0292 Amarok::Logger::longMessage( i18nc("Last.fm: errorMessage", 0293 "Either the username was not recognized, or the password was incorrect." ) ); 0294 break; 0295 0296 default: 0297 Amarok::Logger::longMessage( i18nc("Last.fm: errorMessage", 0298 "There was a problem communicating with the Last.fm services. Please try again later." ) ); 0299 break; 0300 } 0301 m_authenticateReply = nullptr; 0302 0303 // connect back to config updates 0304 connect( m_config.data(), &LastFmServiceConfig::updated, this, &LastFmService::slotReconfigure ); 0305 continueReconfiguring(); 0306 } 0307 0308 void 0309 LastFmService::onGetUserInfo() 0310 { 0311 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() ); 0312 if( !reply ) 0313 warning() << __PRETTY_FUNCTION__ << "null reply!"; 0314 else 0315 reply->deleteLater(); 0316 0317 switch( reply ? reply->error() : QNetworkReply::UnknownNetworkError ) 0318 { 0319 case QNetworkReply::NoError: 0320 { 0321 lastfm::XmlQuery lfm; 0322 if( lfm.parse( reply->readAll() ) ) { 0323 m_country = lfm["user"]["country"].text(); 0324 m_age = lfm["user"]["age"].text(); 0325 m_gender = lfm["user"]["gender"].text(); 0326 m_playcount = lfm["user"]["playcount"].text(); 0327 m_subscriber = lfm["user"]["subscriber"].text() == "1"; 0328 0329 debug() << "profile info " << m_country << " " << m_age << " " << m_gender << " " << m_playcount << " " << m_subscriber; 0330 if( !lfm["user"][ "image" ].text().isEmpty() ) 0331 { 0332 debug() << "profile avatar: " <<lfm["user"][ "image" ].text(); 0333 AvatarDownloader* downloader = new AvatarDownloader(); 0334 QUrl url( lfm["user"][ "image" ].text() ); 0335 downloader->downloadAvatar( m_config->username(), url); 0336 connect( downloader, &AvatarDownloader::avatarDownloaded, 0337 this, &LastFmService::onAvatarDownloaded ); 0338 } 0339 updateProfileInfo(); 0340 } 0341 else 0342 debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); 0343 break; 0344 } 0345 case QNetworkReply::AuthenticationRequiredError: 0346 debug() << "Last.fm: errorMessage: Sorry, we don't recognise that username, or you typed the password incorrectly."; 0347 break; 0348 default: 0349 debug() << "Last.fm: errorMessage: There was a problem communicating with the Last.fm services. Please try again later."; 0350 break; 0351 } 0352 } 0353 0354 void 0355 LastFmService::onAvatarDownloaded( const QString &username, QPixmap avatar ) 0356 { 0357 DEBUG_BLOCK 0358 sender()->deleteLater(); 0359 if( username == m_config->username() && !avatar.isNull() ) 0360 { 0361 LastFmTreeModel* lfm = dynamic_cast<LastFmTreeModel*>( model() ); 0362 if( !lfm ) 0363 return; 0364 0365 int m = lfm->avatarSize(); 0366 avatar = avatar.scaled( m, m, Qt::KeepAspectRatio, Qt::SmoothTransformation ); 0367 lfm->prepareAvatar( avatar, m ); 0368 m_avatar = avatar; 0369 0370 if( m_avatarLabel ) 0371 m_avatarLabel->setPixmap( m_avatar ); 0372 } 0373 } 0374 0375 void 0376 LastFmService::updateEditHint( int index ) 0377 { 0378 if( !m_customStationEdit ) 0379 return; 0380 QString hint; 0381 switch ( index ) { 0382 case 0: 0383 hint = i18n( "Enter an artist name" ); 0384 break; 0385 case 1: 0386 hint = i18n( "Enter a tag" ); 0387 break; 0388 case 2: 0389 hint = i18n( "Enter a Last.fm user name" ); 0390 break; 0391 default: 0392 return; 0393 } 0394 m_customStationEdit->setPlaceholderText( hint ); 0395 } 0396 0397 void 0398 LastFmService::updateProfileInfo() 0399 { 0400 if( m_userinfo ) 0401 { 0402 m_userinfo->setText( i18n( "Username: %1", m_config->username().toHtmlEscaped() ) ); 0403 } 0404 0405 if( m_profile && !m_playcount.isEmpty() ) 0406 { 0407 m_profile->setText( i18np( "Play Count: %1 play", "Play Count: %1 plays", m_playcount.toInt() ) ); 0408 } 0409 } 0410 0411 void 0412 LastFmService::polish() 0413 { 0414 if( !m_polished ) 0415 { 0416 LastFmTreeView* view = new LastFmTreeView( this ); 0417 view->setFrameShape( QFrame::NoFrame ); 0418 view->setDragEnabled ( true ); 0419 view->setSortingEnabled( false ); 0420 view->setDragDropMode ( QAbstractItemView::DragOnly ); 0421 setView( view ); 0422 0423 //m_bottomPanel->setMaximumHeight( 300 ); 0424 m_bottomPanel->hide(); 0425 0426 m_topPanel->setMaximumHeight( 300 ); 0427 BoxWidget * outerProfilebox = new BoxWidget( false, m_topPanel ); 0428 outerProfilebox->layout()->setSpacing(1); 0429 0430 m_avatarLabel = new QLabel(outerProfilebox); 0431 if( !m_avatar ) 0432 { 0433 int m = LastFmTreeModel::avatarSize(); 0434 m_avatarLabel->setPixmap( QIcon::fromTheme( "filename-artist-amarok" ).pixmap(m, m) ); 0435 m_avatarLabel->setFixedSize( m, m ); 0436 } 0437 else 0438 { 0439 m_avatarLabel->setPixmap( m_avatar ); 0440 m_avatarLabel->setFixedSize( m_avatar.width(), m_avatar.height() ); 0441 m_avatarLabel->setMargin( 5 ); 0442 } 0443 0444 BoxWidget * innerProfilebox = new BoxWidget( true, outerProfilebox ); 0445 innerProfilebox->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum ); 0446 m_userinfo = new QLabel(innerProfilebox); 0447 m_userinfo->setText( m_config->username() ); 0448 m_profile = new QLabel(innerProfilebox); 0449 m_profile->setText(QString()); 0450 updateProfileInfo(); 0451 0452 0453 QGroupBox *customStation = new QGroupBox( i18n( "Create a Custom Last.fm Station" ), m_topPanel ); 0454 m_customStationCombo = new QComboBox; 0455 QStringList choices; 0456 choices << i18n( "Artist" ) << i18n( "Tag" ) << i18n( "User" ); 0457 m_customStationCombo->insertItems(0, choices); 0458 m_customStationEdit = new QLineEdit; 0459 m_customStationEdit->setClearButtonEnabled( true ); 0460 updateEditHint( m_customStationCombo->currentIndex() ); 0461 m_customStationButton = new QPushButton; 0462 m_customStationButton->setObjectName( "customButton" ); 0463 m_customStationButton->setIcon( QIcon::fromTheme( "media-playback-start-amarok" ) ); 0464 QHBoxLayout *hbox = new QHBoxLayout(); 0465 hbox->addWidget(m_customStationCombo); 0466 hbox->addWidget(m_customStationEdit); 0467 hbox->addWidget(m_customStationButton); 0468 customStation->setLayout(hbox); 0469 0470 connect( m_customStationEdit, &QLineEdit::returnPressed, this, &LastFmService::playCustomStation ); 0471 connect( m_customStationButton, &QPushButton::clicked, this, &LastFmService::playCustomStation ); 0472 connect( m_customStationCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), 0473 this, &LastFmService::updateEditHint); 0474 0475 QList<int> levels; 0476 levels << CategoryId::Genre << CategoryId::Album; 0477 m_polished = true; 0478 } 0479 } 0480 0481 void 0482 LastFmService::loveCurrentTrack() 0483 { 0484 love( The::engineController()->currentTrack() ); 0485 } 0486 0487 void 0488 LastFmService::love( Meta::TrackPtr track ) 0489 { 0490 if( m_scrobbler ) 0491 m_scrobbler->loveTrack( track ); 0492 } 0493 0494 void LastFmService::playCustomStation() 0495 { 0496 DEBUG_BLOCK 0497 QString text = m_customStationEdit->text(); 0498 QString station; 0499 debug() << "Selected combo " <<m_customStationCombo->currentIndex(); 0500 switch ( m_customStationCombo->currentIndex() ) { 0501 case 0: 0502 station = "lastfm://artist/" + text + "/similarartists"; 0503 break; 0504 case 1: 0505 station = "lastfm://globaltags/" + text; 0506 break; 0507 case 2: 0508 station = "lastfm://user/" + text + "/personal"; 0509 break; 0510 default: 0511 return; 0512 } 0513 0514 if ( !station.isEmpty() ) { 0515 playLastFmStation( QUrl( station ) ); 0516 } 0517 } 0518 0519 void LastFmService::playLastFmStation( const QUrl &url ) 0520 { 0521 Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url ); 0522 The::playlistController()->insertOptioned( track, Playlist::OnPlayMediaAction ); 0523 } 0524 0525 Collections::Collection * LastFmService::collection() 0526 { 0527 return m_collection; 0528 }