1 /**
2 * Copyright (C) 2012 Martin Sandsmark <martin.sandsmark@kde.org>
3 * Copyright (C) 2014 Arnold Dumas <contact@arnolddumas.fr>
4 *
5 * This program is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU General Public License as published by the Free Software
7 * Foundation; either version 2 of the License, or (at your option) any later
8 * version.
9 *
10 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
11 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
12 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along with
15 * this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "scrobbler.h"
19
20 #include <QCryptographicHash>
21 #include <QDir>
22 #include <QNetworkRequest>
23 #include <QNetworkReply>
24 #include <QDomDocument>
25 #include <QByteArray>
26 #include <QUrl>
27 #include <QUrlQuery>
28
29 #include <kconfiggroup.h>
30 #include <KSharedConfig>
31
32 #include <memory>
33
34 #include "juktag.h"
35 #include "juk.h"
36 #include "juk_debug.h"
37
Scrobbler(QObject * parent)38 Scrobbler::Scrobbler(QObject* parent)
39 : QObject(parent)
40 , m_networkAccessManager(new QNetworkAccessManager(this))
41 , m_wallet(Scrobbler::openKWallet())
42 {
43 QByteArray sessionKey;
44
45 if (m_wallet) {
46 m_wallet->readEntry("SessionKey", sessionKey);
47 } else {
48 KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
49 sessionKey.append(config.readEntry("SessionKey", "").toLatin1());
50 }
51
52 if(sessionKey.isEmpty())
53 getAuthToken();
54 }
55
isScrobblingEnabled()56 bool Scrobbler::isScrobblingEnabled() // static
57 {
58 QString username, password;
59
60 // checks without prompting to open the wallet
61 if (Wallet::folderDoesNotExist(Wallet::LocalWallet(), "JuK")) {
62 KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
63
64 username = config.readEntry("Username", "");
65 password = config.readEntry("Password", "");
66 } else {
67 auto wallet = Scrobbler::openKWallet();
68 if (wallet) {
69 QMap<QString, QString> scrobblingCredentials;
70 wallet->readMap("Scrobbling", scrobblingCredentials);
71
72 if (scrobblingCredentials.contains("Username") && scrobblingCredentials.contains("Password")) {
73 username = scrobblingCredentials["Username"];
74 password = scrobblingCredentials["Password"];
75 }
76 }
77 }
78
79 return (!username.isEmpty() && !password.isEmpty());
80 }
81
openKWallet()82 std::unique_ptr<KWallet::Wallet> Scrobbler::openKWallet() // static
83 {
84 using KWallet::Wallet;
85
86 const QString walletFolderName(QStringLiteral("JuK"));
87 const auto walletName = Wallet::LocalWallet();
88
89 // checks without prompting to open the wallet
90 if (Wallet::folderDoesNotExist(walletName, walletFolderName)) {
91 return nullptr;
92 }
93
94 std::unique_ptr<Wallet> wallet(
95 Wallet::openWallet(walletName, JuK::JuKInstance()->winId()));
96
97 if(!wallet ||
98 (!wallet->hasFolder(walletFolderName) &&
99 !wallet->createFolder(walletFolderName)) ||
100 !wallet->setFolder(walletFolderName))
101 {
102 return nullptr;
103 }
104
105 return wallet;
106 }
107
md5(QByteArray data)108 QByteArray Scrobbler::md5(QByteArray data)
109 {
110 return QCryptographicHash::hash(data, QCryptographicHash::Md5)
111 .toHex().rightJustified(32, '0').toLower();
112 }
113
sign(QMap<QString,QString> & params)114 void Scrobbler::sign(QMap< QString, QString >& params)
115 {
116 params["api_key"] = "3e6ecbd7284883089e8f2b5b53b0aecd";
117
118 QString s;
119 QMapIterator<QString, QString> i(params);
120
121 while(i.hasNext()) {
122 i.next();
123 s += i.key() + i.value();
124 }
125
126 s += "2cab3957b1f70d485e9815ac1ac94096"; //shared secret
127
128 params["api_sig"] = md5(s.toUtf8());
129 }
130
getAuthToken(QString username,QString password)131 void Scrobbler::getAuthToken(QString username, QString password)
132 {
133 qCDebug(JUK_LOG) << "Getting new auth token for user:" << username;
134
135 QByteArray authToken = md5((username + md5(password.toUtf8())).toUtf8());
136
137 QMap<QString, QString> params;
138 params["method"] = "auth.getMobileSession";
139 params["authToken"] = authToken;
140 params["username"] = username;
141
142 QUrl url("https://ws.audioscrobbler.com/2.0/?");
143
144 sign(params);
145
146 QUrlQuery urlQuery;
147 const auto paramKeys = params.keys();
148 for(const auto &key : paramKeys) {
149 urlQuery.addQueryItem(key, params[key]);
150 }
151
152 url.setQuery(urlQuery);
153
154 QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url));
155 connect(reply, SIGNAL(finished()), this, SLOT(handleAuthenticationReply()));
156 }
157
getAuthToken()158 void Scrobbler::getAuthToken()
159 {
160 QString username, password;
161
162 if (m_wallet) {
163
164 QMap<QString, QString> scrobblingCredentials;
165 m_wallet->readMap("Scrobbling", scrobblingCredentials);
166
167 if (scrobblingCredentials.contains("Username") && scrobblingCredentials.contains("Password")) {
168
169 username = scrobblingCredentials["Username"];
170 password = scrobblingCredentials["Password"];
171 }
172
173 } else {
174
175 KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
176 username = config.readEntry("Username", "");
177 password = config.readEntry("Password", "");
178 }
179
180 if(username.isEmpty() || password.isEmpty())
181 return;
182
183 getAuthToken(username, password);
184 }
185
handleAuthenticationReply()186 void Scrobbler::handleAuthenticationReply()
187 {
188 QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
189
190 qCDebug(JUK_LOG) << "got authentication reply";
191 if (reply->error() != QNetworkReply::NoError) {
192 emit invalidAuth();
193 qCWarning(JUK_LOG) << "Error while getting authentication reply" << reply->errorString();
194 return;
195 }
196
197 QDomDocument doc;
198 QByteArray data = reply->readAll();
199 doc.setContent(data);
200
201 QString sessionKey = doc.documentElement()
202 .firstChildElement("session")
203 .firstChildElement("key").text();
204
205 if(sessionKey.isEmpty()) {
206 emit invalidAuth();
207 qCWarning(JUK_LOG) << "Unable to get session key" << data;
208 return;
209 }
210
211 if (m_wallet) {
212
213 m_wallet->writeEntry("SessionKey", sessionKey.toUtf8());
214
215 } else {
216
217 KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
218 config.writeEntry("SessionKey", sessionKey);
219 }
220
221 emit validAuth();
222 }
223
nowPlaying(const FileHandle & file)224 void Scrobbler::nowPlaying(const FileHandle& file)
225 {
226 QString sessionKey;
227
228 if (m_wallet) {
229
230 QByteArray sessionKeyByteArray;
231 m_wallet->readEntry("SessionKey", sessionKeyByteArray);
232 sessionKey = QString::fromLatin1(sessionKeyByteArray);
233
234 } else {
235
236 KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
237 sessionKey = config.readEntry("SessionKey", "");
238 }
239
240 if (!m_file.isNull()) {
241 scrobble(); // Update time-played info for last track
242 }
243
244 QMap<QString, QString> params;
245 params["method"] = "track.updateNowPlaying";
246 params["sk"] = sessionKey;
247 params["track"] = file.tag()->title();
248 params["artist"] = file.tag()->artist();
249 params["album"] = file.tag()->album();
250 params["trackNumber"] = QString::number(file.tag()->track());
251 params["duration"] = QString::number(file.tag()->seconds());
252
253 sign(params);
254 post(params);
255
256 m_file = file; // May be empty FileHandle
257 m_playbackTimer = QDateTime::currentDateTime();
258 }
259
scrobble()260 void Scrobbler::scrobble()
261 {
262 QString sessionKey;
263
264 if (m_wallet) {
265
266 QByteArray sessionKeyByteArray;
267 m_wallet->readEntry("SessionKey", sessionKeyByteArray);
268 sessionKey = QString::fromLatin1(sessionKeyByteArray);
269
270 } else {
271
272 KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
273 sessionKey = config.readEntry("SessionKey", "");
274 }
275
276 if(sessionKey.isEmpty()) {
277 getAuthToken();
278 return;
279 }
280
281 int halfDuration = m_file.tag()->seconds() / 2;
282 int timeElapsed = m_playbackTimer.secsTo(QDateTime::currentDateTime());
283
284 if (timeElapsed < 30 || timeElapsed < halfDuration) {
285 return; // API says not to scrobble if the user didn't play long enough
286 }
287
288 qCDebug(JUK_LOG) << "Scrobbling" << m_file.tag()->title();
289
290 QMap<QString, QString> params;
291 params["method"] = "track.scrobble";
292 params["sk"] = sessionKey;
293 params["track"] = m_file.tag()->title();
294 params["artist"] = m_file.tag()->artist();
295 params["album"] = m_file.tag()->album();
296 params["timestamp"] = QString::number(m_playbackTimer.toSecsSinceEpoch());
297 params["trackNumber"] = QString::number(m_file.tag()->track());
298 params["duration"] = QString::number(m_file.tag()->seconds());
299
300 sign(params);
301 post(params);
302 }
303
post(QMap<QString,QString> & params)304 void Scrobbler::post(QMap<QString, QString> ¶ms)
305 {
306 QUrl url("https://ws.audioscrobbler.com/2.0/");
307
308 QByteArray data;
309 const auto paramKeys = params.keys();
310 for(const auto &key : paramKeys) {
311 data += QUrl::toPercentEncoding(key) + '=' + QUrl::toPercentEncoding(params[key]) + '&';
312 }
313
314 QNetworkRequest req(url);
315 req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
316 QNetworkReply *reply = m_networkAccessManager->post(req, data);
317 connect(reply, SIGNAL(finished()), this, SLOT(handleResults()));
318 }
319
handleResults()320 void Scrobbler::handleResults()
321 {
322 QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
323 QByteArray data = reply->readAll();
324 if(data.contains("code=\"9\"")) // We need a new token
325 getAuthToken();
326 }
327