File indexing completed on 2024-05-19 04:50:17

0001 /****************************************************************************************
0002  * Copyright (c) 2007 Shane King <kde@dontletsstart.com>                                *
0003  * Copyright (c) 2008 Leo Franchi <lfranchi@kde.org>                                    *
0004  * Copyright (c) 2012 Matěj Laitl <matej@laitlcz>                                       *
0005  * Copyright (c) 2013 Vedant Agarwala <vedant.kota@gmail.com>                           *
0006  *                                                                                      *
0007  * This program is free software; you can redistribute it and/or modify it under        *
0008  * the terms of the GNU General Public License as published by the Free Software        *
0009  * Foundation; either version 2 of the License, or (at your option) any later           *
0010  * version.                                                                             *
0011  *                                                                                      *
0012  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0013  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0014  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0015  *                                                                                      *
0016  * You should have received a copy of the GNU General Public License along with         *
0017  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0018  ****************************************************************************************/
0019 
0020 #define DEBUG_PREFIX "lastfm"
0021 
0022 #include "ScrobblerAdapter.h"
0023 
0024 #include "MainWindow.h"
0025 #include "core/collections/Collection.h"
0026 #include "core/logger/Logger.h"
0027 #include "core/meta/Meta.h"
0028 #include "core/meta/support/MetaConstants.h"
0029 #include "core/support/Components.h"
0030 #include "core/support/Debug.h"
0031 
0032 #include <KLocalizedString>
0033 
0034 #include <QNetworkReply>
0035 
0036 #include <misc.h>
0037 
0038 ScrobblerAdapter::ScrobblerAdapter( const QString &clientId, const LastFmServiceConfigPtr &config )
0039     : m_scrobbler( clientId )
0040     , m_config( config )
0041 {
0042     // work around a bug in liblastfm -- -it doesn't create its config dir, so when it
0043     // tries to write the track cache, it fails silently. Last check: liblastfm 1.0.!
0044     QList<QDir> dirs;
0045     dirs << lastfm::dir::runtimeData() << lastfm::dir::cache() << lastfm::dir::logs();
0046     foreach( const QDir &dir, dirs )
0047     {
0048         if( !dir.exists() )
0049         {
0050             debug() << "creating" << dir.absolutePath() << "directory for liblastfm";
0051             dir.mkpath( "." );
0052         }
0053     }
0054 
0055     connect( The::mainWindow(), &MainWindow::loveTrack,
0056              this, &ScrobblerAdapter::loveTrack );
0057     connect( The::mainWindow(), &MainWindow::banTrack,
0058              this, &ScrobblerAdapter::banTrack );
0059 
0060     connect( &m_scrobbler, &lastfm::Audioscrobbler::scrobblesSubmitted,
0061              this, &ScrobblerAdapter::slotScrobblesSubmitted );
0062     connect( &m_scrobbler, &lastfm::Audioscrobbler::nowPlayingError,
0063              this, &ScrobblerAdapter::slotNowPlayingError );
0064 }
0065 
0066 ScrobblerAdapter::~ScrobblerAdapter()
0067 {
0068 }
0069 
0070 QString
0071 ScrobblerAdapter::prettyName() const
0072 {
0073     return i18n( "Last.fm" );
0074 }
0075 
0076 StatSyncing::ScrobblingService::ScrobbleError
0077 ScrobblerAdapter::scrobble( const Meta::TrackPtr &track, double playedFraction,
0078                             const QDateTime &time )
0079 {
0080     Q_ASSERT( track );
0081     if( isToBeSkipped( track ) )
0082     {
0083         debug() << "scrobble(): refusing track" << track->prettyUrl()
0084                 << "- contains label:" << m_config->filteredLabel() << "which is marked to be skipped";
0085         return SkippedByUser;
0086     }
0087     if( track->length() * qMin( 1.0, playedFraction ) < 30 * 1000 )
0088     {
0089         debug() << "scrobble(): refusing track" << track->prettyUrl() << "- played time ("
0090                 << track->length() / 1000 << "*" << playedFraction << "s) shorter than 30 s";
0091         return TooShort;
0092     }
0093     int playcount = qRound( playedFraction );
0094     if( playcount <= 0 )
0095     {
0096         debug() << "scrobble(): refusing track" << track->prettyUrl() << "- played "
0097                 << "fraction (" << playedFraction * 100 << "%) less than 50 %";
0098         return TooShort;
0099     }
0100 
0101     lastfm::MutableTrack lfmTrack;
0102     copyTrackMetadata( lfmTrack, track );
0103     // since liblastfm >= 1.0.3 it interprets following extra property:
0104     lfmTrack.setExtra( "playCount", QString::number( playcount ) );
0105     lfmTrack.setTimeStamp( time.isValid() ? time : QDateTime::currentDateTime() );
0106     debug() << "scrobble: " << lfmTrack.artist() << "-" << lfmTrack.album() << "-"
0107             << lfmTrack.title() << "source:" << lfmTrack.source() << "duration:"
0108             << lfmTrack.duration();
0109     m_scrobbler.cache( lfmTrack );
0110     m_scrobbler.submit(); // since liblastfm 1.0.7, submit() is not called automatically upon cache()
0111     switch( lfmTrack.scrobbleStatus() )
0112     {
0113         case lastfm::Track::Cached:
0114         case lastfm::Track::Submitted:
0115             return NoError;
0116         case lastfm::Track::Null:
0117         case lastfm::Track::Error:
0118             break;
0119     }
0120     return BadMetadata;
0121 }
0122 
0123 void
0124 ScrobblerAdapter::updateNowPlaying( const Meta::TrackPtr &track )
0125 {
0126     lastfm::MutableTrack lfmTrack;
0127     if( track )
0128     {
0129         if( isToBeSkipped( track ) )
0130         {
0131             debug() << "updateNowPlaying(): refusing track" << track->prettyUrl()
0132                     << "- contains label:" << m_config->filteredLabel() << "which is marked to be skipped";
0133             return;
0134         }
0135         copyTrackMetadata( lfmTrack, track );
0136         debug() << "nowPlaying: " << lfmTrack.artist() << "-" << lfmTrack.album() << "-"
0137                 << lfmTrack.title() << "source:" << lfmTrack.source() << "duration:"
0138                 << lfmTrack.duration();
0139         m_scrobbler.nowPlaying( lfmTrack );
0140     }
0141     else
0142     {
0143         debug() << "removeNowPlaying";
0144         QNetworkReply *reply = lfmTrack.removeNowPlaying(); // works even with empty lfmTrack
0145         connect( reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater ); // don't leak
0146     }
0147 }
0148 
0149 void
0150 ScrobblerAdapter::loveTrack( const Meta::TrackPtr &track ) // slot
0151 {
0152     if( !track )
0153         return;
0154 
0155     lastfm::MutableTrack trackInfo;
0156     copyTrackMetadata( trackInfo, track );
0157     trackInfo.love();
0158     Amarok::Logger::shortMessage( i18nc( "As in Last.fm", "Loved Track: %1", track->prettyName() ) );
0159 }
0160 
0161 void
0162 ScrobblerAdapter::banTrack( const Meta::TrackPtr &track ) // slot
0163 {
0164     if( !track )
0165         return;
0166 
0167     lastfm::MutableTrack trackInfo;
0168     copyTrackMetadata( trackInfo, track );
0169     trackInfo.ban();
0170     Amarok::Logger::shortMessage( i18nc( "As in Last.fm", "Banned Track: %1", track->prettyName() ) );
0171 }
0172 
0173 void
0174 ScrobblerAdapter::slotScrobblesSubmitted( const QList<lastfm::Track> &tracks )
0175 {
0176     foreach( const lastfm::Track &track, tracks )
0177     {
0178         switch( track.scrobbleStatus() )
0179         {
0180             case lastfm::Track::Null:
0181                 warning() << "slotScrobblesSubmitted(): track" << track
0182                           << "has Null scrobble status, strange";
0183                 break;
0184             case lastfm::Track::Cached:
0185                 warning() << "slotScrobblesSubmitted(): track" << track
0186                           << "has Cached scrobble status, strange";
0187                 break;
0188             case lastfm::Track::Submitted:
0189                 if( track.corrected() && m_config->announceCorrections() )
0190                     announceTrackCorrections( track );
0191                 break;
0192             case lastfm::Track::Error:
0193                 warning() << "slotScrobblesSubmitted(): error scrobbling track" << track
0194                           << ":" << track.scrobbleErrorText();
0195                 break;
0196         }
0197     }
0198 }
0199 
0200 void
0201 ScrobblerAdapter::slotNowPlayingError( int code, const QString &message )
0202 {
0203     Q_UNUSED( code )
0204     warning() << "error updating Now Playing status:" << message;
0205 }
0206 
0207 void
0208 ScrobblerAdapter::copyTrackMetadata( lastfm::MutableTrack &to, const Meta::TrackPtr &track )
0209 {
0210     to.setTitle( track->name() );
0211 
0212     QString artistOrComposer;
0213     Meta::ComposerPtr composer = track->composer();
0214     if( m_config->scrobbleComposer() && composer )
0215         artistOrComposer = composer->name();
0216     Meta::ArtistPtr artist = track->artist();
0217     if( artistOrComposer.isEmpty() && artist )
0218         artistOrComposer = artist->name();
0219     to.setArtist( artistOrComposer );
0220 
0221     Meta::AlbumPtr album = track->album();
0222     Meta::ArtistPtr albumArtist;
0223     if( album )
0224     {
0225         to.setAlbum( album->name() );
0226         albumArtist = album->hasAlbumArtist() ? album->albumArtist() : Meta::ArtistPtr();
0227     }
0228     if( albumArtist )
0229         to.setAlbumArtist( albumArtist->name() );
0230 
0231     to.setDuration( track->length() / 1000 );
0232     if( track->trackNumber() >= 0 )
0233         to.setTrackNumber( track->trackNumber() );
0234 
0235     lastfm::Track::Source source = lastfm::Track::Player;
0236     if( track->type() == "stream/lastfm" )
0237         source = lastfm::Track::LastFmRadio;
0238     else if( track->type().startsWith( "stream" ) )
0239         source = lastfm::Track::NonPersonalisedBroadcast;
0240     else if( track->collection() && track->collection()->collectionId() != "localCollection" )
0241         source = lastfm::Track::MediaDevice;
0242     to.setSource( source );
0243 }
0244 
0245 static QString
0246 printCorrected( qint64 field, const QString &original, const QString &corrected )
0247 {
0248     if( corrected.isEmpty() || original == corrected )
0249         return QString();
0250     return i18nc( "%1 is field name such as Album Name; %2 is the original value; %3 is "
0251                   "the corrected value", "%1 <b>%2</b> should be corrected to "
0252                   "<b>%3</b>", Meta::i18nForField( field ), original, corrected );
0253 }
0254 
0255 static QString
0256 printCorrected( qint64 field, const lastfm::AbstractType &original, const lastfm::AbstractType &corrected )
0257 {
0258     return printCorrected( field, original.toString(), corrected.toString() );
0259 }
0260 
0261 void
0262 ScrobblerAdapter::announceTrackCorrections( const lastfm::Track &track )
0263 {
0264     static const lastfm::Track::Corrections orig = lastfm::Track::Original;
0265     static const lastfm::Track::Corrections correct = lastfm::Track::Corrected;
0266 
0267     QString trackName = i18nc( "%1 is artist, %2 is title", "%1 - %2",
0268                                track.artist().name(), track.title() );
0269     QStringList lines;
0270     lines << i18n( "Last.fm suggests that some tags of track <b>%1</b> should be "
0271                    "corrected:", trackName );
0272     QString line;
0273     line = printCorrected( Meta::valTitle, track.title( orig ), track.title( correct ) );
0274     if( !line.isEmpty() )
0275         lines << line;
0276     line = printCorrected( Meta::valAlbum, track.album( orig ), track.album( correct ) );
0277     if( !line.isEmpty() )
0278         lines << line;
0279     line = printCorrected( Meta::valArtist, track.artist( orig ), track.artist( correct ) );
0280     if( !line.isEmpty() )
0281         lines << line;
0282     line = printCorrected( Meta::valAlbumArtist, track.albumArtist( orig ), track.albumArtist( correct ) );
0283     if( !line.isEmpty() )
0284         lines << line;
0285     Amarok::Logger::longMessage( lines.join( "<br>" ) );
0286 }
0287 
0288 bool
0289 ScrobblerAdapter::isToBeSkipped( const Meta::TrackPtr &track ) const
0290 {
0291     Q_ASSERT( track );
0292     if( !m_config->filterByLabel() )
0293         return false;
0294     foreach( const Meta::LabelPtr &label, track->labels() )
0295         if( label->name() == m_config->filteredLabel() )
0296             return true;
0297     return false;
0298 }