File indexing completed on 2023-05-30 11:30:51

0001 /**
0002  * Copyright (C) 2012 Martin Sandsmark <martin.sandsmark@kde.org>
0003  * Copyright (C) 2014 Arnold Dumas <contact@arnolddumas.fr>
0004  *
0005  * This program is free software; you can redistribute it and/or modify it under
0006  * the terms of the GNU General Public License as published by the Free Software
0007  * Foundation; either version 2 of the License, or (at your option) any later
0008  * version.
0009  *
0010  * This program is distributed in the hope that it will be useful, but WITHOUT ANY
0011  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
0012  * PARTICULAR PURPOSE. See the GNU General Public License for more details.
0013  *
0014  * You should have received a copy of the GNU General Public License along with
0015  * this program.  If not, see <http://www.gnu.org/licenses/>.
0016  */
0017 
0018 #include "scrobbler.h"
0019 
0020 #include <QByteArray>
0021 #include <QCryptographicHash>
0022 #include <QDomDocument>
0023 #include <QNetworkReply>
0024 #include <QNetworkRequest>
0025 #include <QUrl>
0026 #include <QUrlQuery>
0027 
0028 #include <kconfiggroup.h>
0029 #include <KSharedConfig>
0030 
0031 #include <memory>
0032 
0033 #include "juktag.h"
0034 #include "juk.h"
0035 #include "juk_debug.h"
0036 
0037 Scrobbler::Scrobbler(QObject* parent)
0038     : QObject(parent)
0039     , m_networkAccessManager(new QNetworkAccessManager(this))
0040     , m_wallet(Scrobbler::openKWallet())
0041 {
0042     QByteArray sessionKey;
0043 
0044     if (m_wallet) {
0045         m_wallet->readEntry("SessionKey", sessionKey);
0046     } else {
0047         KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
0048         sessionKey.append(config.readEntry("SessionKey", "").toLatin1());
0049     }
0050 
0051     if(sessionKey.isEmpty())
0052         getAuthToken();
0053 }
0054 
0055 bool Scrobbler::isScrobblingEnabled() // static
0056 {
0057     QString username, password;
0058 
0059     // checks without prompting to open the wallet
0060     if (Wallet::folderDoesNotExist(Wallet::LocalWallet(), "JuK")) {
0061         KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
0062 
0063         username = config.readEntry("Username", "");
0064         password = config.readEntry("Password", "");
0065     } else {
0066         auto wallet = Scrobbler::openKWallet();
0067         if (wallet) {
0068             QMap<QString, QString> scrobblingCredentials;
0069             wallet->readMap("Scrobbling", scrobblingCredentials);
0070 
0071             if (scrobblingCredentials.contains("Username") && scrobblingCredentials.contains("Password")) {
0072                 username = scrobblingCredentials["Username"];
0073                 password = scrobblingCredentials["Password"];
0074             }
0075         }
0076     }
0077 
0078     return (!username.isEmpty() && !password.isEmpty());
0079 }
0080 
0081 std::unique_ptr<KWallet::Wallet> Scrobbler::openKWallet() // static
0082 {
0083     using KWallet::Wallet;
0084 
0085     const QString walletFolderName(QStringLiteral("JuK"));
0086     const auto walletName = Wallet::LocalWallet();
0087 
0088     // checks without prompting to open the wallet
0089     if (Wallet::folderDoesNotExist(walletName, walletFolderName)) {
0090         return nullptr;
0091     }
0092 
0093     std::unique_ptr<Wallet> wallet(
0094         Wallet::openWallet(walletName, JuK::JuKInstance()->winId()));
0095 
0096     if(!wallet ||
0097        (!wallet->hasFolder(walletFolderName) &&
0098            !wallet->createFolder(walletFolderName)) ||
0099        !wallet->setFolder(walletFolderName))
0100     {
0101         return nullptr;
0102     }
0103 
0104     return wallet;
0105 }
0106 
0107 QByteArray Scrobbler::md5(QByteArray data)
0108 {
0109     return QCryptographicHash::hash(data, QCryptographicHash::Md5)
0110         .toHex().rightJustified(32, '0').toLower();
0111 }
0112 
0113 void Scrobbler::sign(QMap< QString, QString >& params)
0114 {
0115     params["api_key"] = "3e6ecbd7284883089e8f2b5b53b0aecd";
0116 
0117     QString s;
0118     QMapIterator<QString, QString> i(params);
0119 
0120     while(i.hasNext()) {
0121         i.next();
0122         s += i.key() + i.value();
0123     }
0124 
0125     s += "2cab3957b1f70d485e9815ac1ac94096"; //shared secret
0126 
0127     params["api_sig"] = md5(s.toUtf8());
0128 }
0129 
0130 void Scrobbler::getAuthToken(QString username, QString password)
0131 {
0132     qCDebug(JUK_LOG) << "Getting new auth token for user:" << username;
0133 
0134     QByteArray authToken = md5((username + md5(password.toUtf8())).toUtf8());
0135 
0136     QMap<QString, QString> params;
0137     params["method"]    = "auth.getMobileSession";
0138     params["authToken"] = authToken;
0139     params["username"]  = username;
0140 
0141     QUrl url("https://ws.audioscrobbler.com/2.0/?");
0142 
0143     sign(params);
0144 
0145     QUrlQuery urlQuery;
0146     const auto paramKeys = params.keys();
0147     for(const auto &key : paramKeys) {
0148         urlQuery.addQueryItem(key, params[key]);
0149     }
0150 
0151     url.setQuery(urlQuery);
0152 
0153     QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url));
0154     connect(reply, SIGNAL(finished()), this, SLOT(handleAuthenticationReply()));
0155 }
0156 
0157 void Scrobbler::getAuthToken()
0158 {
0159     QString username, password;
0160 
0161     if (m_wallet) {
0162 
0163         QMap<QString, QString> scrobblingCredentials;
0164         m_wallet->readMap("Scrobbling", scrobblingCredentials);
0165 
0166         if (scrobblingCredentials.contains("Username") && scrobblingCredentials.contains("Password")) {
0167 
0168             username = scrobblingCredentials["Username"];
0169             password = scrobblingCredentials["Password"];
0170         }
0171 
0172     } else {
0173 
0174         KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
0175         username = config.readEntry("Username", "");
0176         password = config.readEntry("Password", "");
0177     }
0178 
0179     if(username.isEmpty() || password.isEmpty())
0180         return;
0181 
0182     getAuthToken(username, password);
0183 }
0184 
0185 void Scrobbler::handleAuthenticationReply()
0186 {
0187     QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
0188 
0189     qCDebug(JUK_LOG) << "got authentication reply";
0190     if (reply->error() != QNetworkReply::NoError) {
0191         emit invalidAuth();
0192         qCWarning(JUK_LOG) << "Error while getting authentication reply" << reply->errorString();
0193         return;
0194     }
0195 
0196     QDomDocument doc;
0197     QByteArray data = reply->readAll();
0198     doc.setContent(data);
0199 
0200     QString sessionKey = doc.documentElement()
0201         .firstChildElement("session")
0202             .firstChildElement("key").text();
0203 
0204     if(sessionKey.isEmpty()) {
0205         emit invalidAuth();
0206         qCWarning(JUK_LOG) << "Unable to get session key" << data;
0207         return;
0208     }
0209 
0210     if (m_wallet) {
0211 
0212         m_wallet->writeEntry("SessionKey", sessionKey.toUtf8());
0213 
0214     } else {
0215 
0216         KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
0217         config.writeEntry("SessionKey", sessionKey);
0218     }
0219 
0220     emit validAuth();
0221 }
0222 
0223 void Scrobbler::nowPlaying(const FileHandle& file)
0224 {
0225     QString sessionKey;
0226 
0227     if (m_wallet) {
0228 
0229         QByteArray sessionKeyByteArray;
0230         m_wallet->readEntry("SessionKey", sessionKeyByteArray);
0231         sessionKey = QString::fromLatin1(sessionKeyByteArray);
0232 
0233     } else {
0234 
0235         KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
0236         sessionKey = config.readEntry("SessionKey", "");
0237     }
0238 
0239     if (!m_file.isNull()) {
0240         scrobble(); // Update time-played info for last track
0241     }
0242 
0243     QMap<QString, QString> params;
0244     params["method"] = "track.updateNowPlaying";
0245     params["sk"]     = sessionKey;
0246     params["track"]  = file.tag()->title();
0247     params["artist"] = file.tag()->artist();
0248     params["album"]  = file.tag()->album();
0249     params["trackNumber"] = QString::number(file.tag()->track());
0250     params["duration"]    = QString::number(file.tag()->seconds());
0251 
0252     sign(params);
0253     post(params);
0254 
0255     m_file = file; // May be empty FileHandle
0256     m_playbackTimer = QDateTime::currentDateTime();
0257 }
0258 
0259 void Scrobbler::scrobble()
0260 {
0261     QString sessionKey;
0262 
0263     if (m_wallet) {
0264 
0265         QByteArray sessionKeyByteArray;
0266         m_wallet->readEntry("SessionKey", sessionKeyByteArray);
0267         sessionKey = QString::fromLatin1(sessionKeyByteArray);
0268 
0269     } else {
0270 
0271         KConfigGroup config(KSharedConfig::openConfig(), "Scrobbling");
0272         sessionKey = config.readEntry("SessionKey", "");
0273     }
0274 
0275     if(sessionKey.isEmpty()) {
0276         getAuthToken();
0277         return;
0278     }
0279 
0280     int halfDuration = m_file.tag()->seconds() / 2;
0281     int timeElapsed = m_playbackTimer.secsTo(QDateTime::currentDateTime());
0282 
0283     if (timeElapsed < 30 || timeElapsed < halfDuration) {
0284         return; // API says not to scrobble if the user didn't play long enough
0285     }
0286 
0287     qCDebug(JUK_LOG) << "Scrobbling" << m_file.tag()->title();
0288 
0289     QMap<QString, QString> params;
0290     params["method"] = "track.scrobble";
0291     params["sk"]     = sessionKey;
0292     params["track"]  = m_file.tag()->title();
0293     params["artist"] = m_file.tag()->artist();
0294     params["album"]  = m_file.tag()->album();
0295     params["timestamp"]   = QString::number(m_playbackTimer.toSecsSinceEpoch());
0296     params["trackNumber"] = QString::number(m_file.tag()->track());
0297     params["duration"]    = QString::number(m_file.tag()->seconds());
0298 
0299     sign(params);
0300     post(params);
0301 }
0302 
0303 void Scrobbler::post(QMap<QString, QString> &params)
0304 {
0305     QUrl url("https://ws.audioscrobbler.com/2.0/");
0306 
0307     QByteArray data;
0308     const auto paramKeys = params.keys();
0309     for(const auto &key : paramKeys) {
0310         data += QUrl::toPercentEncoding(key) + '=' + QUrl::toPercentEncoding(params[key]) + '&';
0311     }
0312 
0313     QNetworkRequest req(url);
0314     req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
0315     QNetworkReply *reply = m_networkAccessManager->post(req, data);
0316     connect(reply, SIGNAL(finished()), this, SLOT(handleResults()));
0317 }
0318 
0319 void Scrobbler::handleResults()
0320 {
0321     QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
0322     QByteArray data = reply->readAll();
0323     if(data.contains("code=\"9\"")) // We need a new token
0324         getAuthToken();
0325 }