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