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> ¶ms) 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 }