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 }