File indexing completed on 2021-12-21 13:28:00

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