1 /****************************************************************************************
2  * Copyright (c) 2007 Shane King <kde@dontletsstart.com>                                *
3  * Copyright (c) 2008 Leo Franchi <lfranchi@kde.org>                                    *
4  * Copyright (c) 2012 Matěj Laitl <matej@laitlcz>                                       *
5  * Copyright (c) 2013 Vedant Agarwala <vedant.kota@gmail.com>                           *
6  *                                                                                      *
7  * This program is free software; you can redistribute it and/or modify it under        *
8  * the terms of the GNU General Public License as published by the Free Software        *
9  * Foundation; either version 2 of the License, or (at your option) any later           *
10  * version.                                                                             *
11  *                                                                                      *
12  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
13  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
14  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
15  *                                                                                      *
16  * You should have received a copy of the GNU General Public License along with         *
17  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
18  ****************************************************************************************/
19 
20 #define DEBUG_PREFIX "lastfm"
21 
22 #include "ScrobblerAdapter.h"
23 
24 #include "MainWindow.h"
25 #include "core/collections/Collection.h"
26 #include "core/logger/Logger.h"
27 #include "core/meta/Meta.h"
28 #include "core/meta/support/MetaConstants.h"
29 #include "core/support/Components.h"
30 #include "core/support/Debug.h"
31 
32 #include <KLocalizedString>
33 
34 #include <QNetworkReply>
35 
36 #include <misc.h>
37 
ScrobblerAdapter(const QString & clientId,const LastFmServiceConfigPtr & config)38 ScrobblerAdapter::ScrobblerAdapter( const QString &clientId, const LastFmServiceConfigPtr &config )
39     : m_scrobbler( clientId )
40     , m_config( config )
41 {
42     // work around a bug in liblastfm -- -it doesn't create its config dir, so when it
43     // tries to write the track cache, it fails silently. Last check: liblastfm 1.0.!
44     QList<QDir> dirs;
45     dirs << lastfm::dir::runtimeData() << lastfm::dir::cache() << lastfm::dir::logs();
46     foreach( const QDir &dir, dirs )
47     {
48         if( !dir.exists() )
49         {
50             debug() << "creating" << dir.absolutePath() << "directory for liblastfm";
51             dir.mkpath( "." );
52         }
53     }
54 
55     connect( The::mainWindow(), &MainWindow::loveTrack,
56              this, &ScrobblerAdapter::loveTrack );
57     connect( The::mainWindow(), &MainWindow::banTrack,
58              this, &ScrobblerAdapter::banTrack );
59 
60     connect( &m_scrobbler, &lastfm::Audioscrobbler::scrobblesSubmitted,
61              this, &ScrobblerAdapter::slotScrobblesSubmitted );
62     connect( &m_scrobbler, &lastfm::Audioscrobbler::nowPlayingError,
63              this, &ScrobblerAdapter::slotNowPlayingError );
64 }
65 
~ScrobblerAdapter()66 ScrobblerAdapter::~ScrobblerAdapter()
67 {
68 }
69 
70 QString
prettyName() const71 ScrobblerAdapter::prettyName() const
72 {
73     return i18n( "Last.fm" );
74 }
75 
76 StatSyncing::ScrobblingService::ScrobbleError
scrobble(const Meta::TrackPtr & track,double playedFraction,const QDateTime & time)77 ScrobblerAdapter::scrobble( const Meta::TrackPtr &track, double playedFraction,
78                             const QDateTime &time )
79 {
80     Q_ASSERT( track );
81     if( isToBeSkipped( track ) )
82     {
83         debug() << "scrobble(): refusing track" << track->prettyUrl()
84                 << "- contains label:" << m_config->filteredLabel() << "which is marked to be skipped";
85         return SkippedByUser;
86     }
87     if( track->length() * qMin( 1.0, playedFraction ) < 30 * 1000 )
88     {
89         debug() << "scrobble(): refusing track" << track->prettyUrl() << "- played time ("
90                 << track->length() / 1000 << "*" << playedFraction << "s) shorter than 30 s";
91         return TooShort;
92     }
93     int playcount = qRound( playedFraction );
94     if( playcount <= 0 )
95     {
96         debug() << "scrobble(): refusing track" << track->prettyUrl() << "- played "
97                 << "fraction (" << playedFraction * 100 << "%) less than 50 %";
98         return TooShort;
99     }
100 
101     lastfm::MutableTrack lfmTrack;
102     copyTrackMetadata( lfmTrack, track );
103     // since liblastfm >= 1.0.3 it interprets following extra property:
104     lfmTrack.setExtra( "playCount", QString::number( playcount ) );
105     lfmTrack.setTimeStamp( time.isValid() ? time : QDateTime::currentDateTime() );
106     debug() << "scrobble: " << lfmTrack.artist() << "-" << lfmTrack.album() << "-"
107             << lfmTrack.title() << "source:" << lfmTrack.source() << "duration:"
108             << lfmTrack.duration();
109     m_scrobbler.cache( lfmTrack );
110     m_scrobbler.submit(); // since liblastfm 1.0.7, submit() is not called automatically upon cache()
111     switch( lfmTrack.scrobbleStatus() )
112     {
113         case lastfm::Track::Cached:
114         case lastfm::Track::Submitted:
115             return NoError;
116         case lastfm::Track::Null:
117         case lastfm::Track::Error:
118             break;
119     }
120     return BadMetadata;
121 }
122 
123 void
updateNowPlaying(const Meta::TrackPtr & track)124 ScrobblerAdapter::updateNowPlaying( const Meta::TrackPtr &track )
125 {
126     lastfm::MutableTrack lfmTrack;
127     if( track )
128     {
129         if( isToBeSkipped( track ) )
130         {
131             debug() << "updateNowPlaying(): refusing track" << track->prettyUrl()
132                     << "- contains label:" << m_config->filteredLabel() << "which is marked to be skipped";
133             return;
134         }
135         copyTrackMetadata( lfmTrack, track );
136         debug() << "nowPlaying: " << lfmTrack.artist() << "-" << lfmTrack.album() << "-"
137                 << lfmTrack.title() << "source:" << lfmTrack.source() << "duration:"
138                 << lfmTrack.duration();
139         m_scrobbler.nowPlaying( lfmTrack );
140     }
141     else
142     {
143         debug() << "removeNowPlaying";
144         QNetworkReply *reply = lfmTrack.removeNowPlaying(); // works even with empty lfmTrack
145         connect( reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater ); // don't leak
146     }
147 }
148 
149 void
loveTrack(const Meta::TrackPtr & track)150 ScrobblerAdapter::loveTrack( const Meta::TrackPtr &track ) // slot
151 {
152     if( !track )
153         return;
154 
155     lastfm::MutableTrack trackInfo;
156     copyTrackMetadata( trackInfo, track );
157     trackInfo.love();
158     Amarok::Logger::shortMessage( i18nc( "As in Last.fm", "Loved Track: %1", track->prettyName() ) );
159 }
160 
161 void
banTrack(const Meta::TrackPtr & track)162 ScrobblerAdapter::banTrack( const Meta::TrackPtr &track ) // slot
163 {
164     if( !track )
165         return;
166 
167     lastfm::MutableTrack trackInfo;
168     copyTrackMetadata( trackInfo, track );
169     trackInfo.ban();
170     Amarok::Logger::shortMessage( i18nc( "As in Last.fm", "Banned Track: %1", track->prettyName() ) );
171 }
172 
173 void
slotScrobblesSubmitted(const QList<lastfm::Track> & tracks)174 ScrobblerAdapter::slotScrobblesSubmitted( const QList<lastfm::Track> &tracks )
175 {
176     foreach( const lastfm::Track &track, tracks )
177     {
178         switch( track.scrobbleStatus() )
179         {
180             case lastfm::Track::Null:
181                 warning() << "slotScrobblesSubmitted(): track" << track
182                           << "has Null scrobble status, strange";
183                 break;
184             case lastfm::Track::Cached:
185                 warning() << "slotScrobblesSubmitted(): track" << track
186                           << "has Cached scrobble status, strange";
187                 break;
188             case lastfm::Track::Submitted:
189                 if( track.corrected() && m_config->announceCorrections() )
190                     announceTrackCorrections( track );
191                 break;
192             case lastfm::Track::Error:
193                 warning() << "slotScrobblesSubmitted(): error scrobbling track" << track
194                           << ":" << track.scrobbleErrorText();
195                 break;
196         }
197     }
198 }
199 
200 void
slotNowPlayingError(int code,const QString & message)201 ScrobblerAdapter::slotNowPlayingError( int code, const QString &message )
202 {
203     Q_UNUSED( code )
204     warning() << "error updating Now Playing status:" << message;
205 }
206 
207 void
copyTrackMetadata(lastfm::MutableTrack & to,const Meta::TrackPtr & track)208 ScrobblerAdapter::copyTrackMetadata( lastfm::MutableTrack &to, const Meta::TrackPtr &track )
209 {
210     to.setTitle( track->name() );
211 
212     QString artistOrComposer;
213     Meta::ComposerPtr composer = track->composer();
214     if( m_config->scrobbleComposer() && composer )
215         artistOrComposer = composer->name();
216     Meta::ArtistPtr artist = track->artist();
217     if( artistOrComposer.isEmpty() && artist )
218         artistOrComposer = artist->name();
219     to.setArtist( artistOrComposer );
220 
221     Meta::AlbumPtr album = track->album();
222     Meta::ArtistPtr albumArtist;
223     if( album )
224     {
225         to.setAlbum( album->name() );
226         albumArtist = album->hasAlbumArtist() ? album->albumArtist() : Meta::ArtistPtr();
227     }
228     if( albumArtist )
229         to.setAlbumArtist( albumArtist->name() );
230 
231     to.setDuration( track->length() / 1000 );
232     if( track->trackNumber() >= 0 )
233         to.setTrackNumber( track->trackNumber() );
234 
235     lastfm::Track::Source source = lastfm::Track::Player;
236     if( track->type() == "stream/lastfm" )
237         source = lastfm::Track::LastFmRadio;
238     else if( track->type().startsWith( "stream" ) )
239         source = lastfm::Track::NonPersonalisedBroadcast;
240     else if( track->collection() && track->collection()->collectionId() != "localCollection" )
241         source = lastfm::Track::MediaDevice;
242     to.setSource( source );
243 }
244 
245 static QString
printCorrected(qint64 field,const QString & original,const QString & corrected)246 printCorrected( qint64 field, const QString &original, const QString &corrected )
247 {
248     if( corrected.isEmpty() || original == corrected )
249         return QString();
250     return i18nc( "%1 is field name such as Album Name; %2 is the original value; %3 is "
251                   "the corrected value", "%1 <b>%2</b> should be corrected to "
252                   "<b>%3</b>", Meta::i18nForField( field ), original, corrected );
253 }
254 
255 static QString
printCorrected(qint64 field,const lastfm::AbstractType & original,const lastfm::AbstractType & corrected)256 printCorrected( qint64 field, const lastfm::AbstractType &original, const lastfm::AbstractType &corrected )
257 {
258     return printCorrected( field, original.toString(), corrected.toString() );
259 }
260 
261 void
announceTrackCorrections(const lastfm::Track & track)262 ScrobblerAdapter::announceTrackCorrections( const lastfm::Track &track )
263 {
264     static const lastfm::Track::Corrections orig = lastfm::Track::Original;
265     static const lastfm::Track::Corrections correct = lastfm::Track::Corrected;
266 
267     QString trackName = i18nc( "%1 is artist, %2 is title", "%1 - %2",
268                                track.artist().name(), track.title() );
269     QStringList lines;
270     lines << i18n( "Last.fm suggests that some tags of track <b>%1</b> should be "
271                    "corrected:", trackName );
272     QString line;
273     line = printCorrected( Meta::valTitle, track.title( orig ), track.title( correct ) );
274     if( !line.isEmpty() )
275         lines << line;
276     line = printCorrected( Meta::valAlbum, track.album( orig ), track.album( correct ) );
277     if( !line.isEmpty() )
278         lines << line;
279     line = printCorrected( Meta::valArtist, track.artist( orig ), track.artist( correct ) );
280     if( !line.isEmpty() )
281         lines << line;
282     line = printCorrected( Meta::valAlbumArtist, track.albumArtist( orig ), track.albumArtist( correct ) );
283     if( !line.isEmpty() )
284         lines << line;
285     Amarok::Logger::longMessage( lines.join( "<br>" ) );
286 }
287 
288 bool
isToBeSkipped(const Meta::TrackPtr & track) const289 ScrobblerAdapter::isToBeSkipped( const Meta::TrackPtr &track ) const
290 {
291     Q_ASSERT( track );
292     if( !m_config->filterByLabel() )
293         return false;
294     foreach( const Meta::LabelPtr &label, track->labels() )
295         if( label->name() == m_config->filteredLabel() )
296             return true;
297     return false;
298 }
299