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> &params)
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