diff options
author | Yoann Lopes <yoann.lopes@nokia.com> | 2011-11-05 00:17:31 +0100 |
---|---|---|
committer | Yoann Lopes <yoann.lopes@nokia.com> | 2011-11-05 00:17:31 +0100 |
commit | 1db565ca96009ceb7e1208823141e6f768afcd86 (patch) | |
tree | 3ffc5810d5249f8f229aeff829d6d93b9bfc1a3a /liblastfm/ws.cpp | |
parent | 97e08a249e8324bf2525fc96961fcfd5212d8da6 (diff) |
Added last.fm scrobbling.
Diffstat (limited to 'liblastfm/ws.cpp')
-rwxr-xr-x | liblastfm/ws.cpp | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/liblastfm/ws.cpp b/liblastfm/ws.cpp new file mode 100755 index 0000000..4523298 --- /dev/null +++ b/liblastfm/ws.cpp @@ -0,0 +1,280 @@ +/* + Copyright 2009 Last.fm Ltd. + - Primarily authored by Max Howell, Jono Cole and Doug Mansell + + This file is part of liblastfm. + + liblastfm is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + liblastfm is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with liblastfm. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "ws.h" +#include "misc.h" +#include "NetworkAccessManager.h" +#include <QCoreApplication> +#include <QDomDocument> +#include <QDomElement> +#include <QLocale> +#include <QStringList> +#include <QUrl> +static QNetworkAccessManager* nam = 0; + + +static inline QString host() +{ + static QStringList const args = QCoreApplication::arguments(); + if (args.contains( "--debug")) + return "ws.staging.audioscrobbler.com"; + + int const n = args.indexOf( "--host" ); + if (n != -1 && args.count() > n+1) + return args[n+1]; + + return LASTFM_WS_HOSTNAME; +} + +static QUrl url() +{ + QUrl url; + url.setScheme( "http" ); + url.setHost( host() ); + url.setPath( "/2.0/" ); + return url; +} + +static QString iso639() +{ + return QLocale().name().left( 2 ).toLower(); +} + +void autograph( QMap<QString, QString>& params ) +{ + params["api_key"] = lastfm::ws::ApiKey; + params["lang"] = iso639(); +} + +void sign( QMap<QString, QString>& params ) +{ + autograph( params ); + // it's allowed for sk to be null if we this is an auth call for instance + if (lastfm::ws::SessionKey.size()) + params["sk"] = lastfm::ws::SessionKey; + + QString s; + QMapIterator<QString, QString> i( params ); + while (i.hasNext()) { + i.next(); + s += i.key() + i.value(); + } + s += lastfm::ws::SharedSecret; + + params["api_sig"] = lastfm::md5( s.toUtf8() ); +} + +lastfm::ws::ParseError::ParseError( lastfm::ws::Error e ) + : std::runtime_error("lastfm::ws::Error"), + e(e) +{ +} + +lastfm::ws::ParseError::~ParseError() throw() +{ +} + +lastfm::ws::Error lastfm::ws::ParseError::enumValue() const +{ + return e; +} + +QNetworkReply* +lastfm::ws::get( QMap<QString, QString> params ) +{ + autograph( params ); + QUrl url = ::url(); + // Qt setQueryItems doesn't encode a bunch of stuff, so we do it manually + QMapIterator<QString, QString> i( params ); + while (i.hasNext()) { + i.next(); + QByteArray const key = QUrl::toPercentEncoding( i.key() ); + QByteArray const value = QUrl::toPercentEncoding( i.value() ); + url.addEncodedQueryItem( key, value ); + } + + qDebug() << url; + + return nam()->get( QNetworkRequest(url) ); +} + + +QNetworkReply* +lastfm::ws::post( QMap<QString, QString> params ) +{ + sign( params ); + QByteArray query; + QMapIterator<QString, QString> i( params ); + while (i.hasNext()) { + i.next(); + query += QUrl::toPercentEncoding( i.key() ) + + '=' + + QUrl::toPercentEncoding( i.value() ) + + '&'; + } + return nam()->post( QNetworkRequest(url()), query ); +} + + +QByteArray +lastfm::ws::parse( QNetworkReply* reply ) throw( ParseError ) +{ + try + { + QByteArray data = reply->readAll(); + + if (!data.size()) + throw MalformedResponse; + + QDomDocument xml; + xml.setContent( data ); + QDomElement lfm = xml.documentElement(); + + if (lfm.isNull()) + throw MalformedResponse; + + QString const status = lfm.attribute( "status" ); + QDomElement error = lfm.firstChildElement( "error" ); + uint const n = lfm.childNodes().count(); + + // no elements beyond the lfm is perfectably acceptable <-- wtf? + // if (n == 0) // nothing useful in the response + if (status == "failed" || n == 1 && !error.isNull()) + throw error.isNull() + ? MalformedResponse + : Error( error.attribute( "code" ).toUInt() ); + + switch (reply->error()) + { + case QNetworkReply::RemoteHostClosedError: + case QNetworkReply::ConnectionRefusedError: + case QNetworkReply::TimeoutError: + case QNetworkReply::ContentAccessDenied: + case QNetworkReply::ContentOperationNotPermittedError: + case QNetworkReply::UnknownContentError: + case QNetworkReply::ProtocolInvalidOperationError: + case QNetworkReply::ProtocolFailure: + throw TryAgainLater; + + case QNetworkReply::NoError: + default: + break; + } + + //FIXME pretty wasteful to parse XML document twice.. + return data; + } + catch (Error e) + { + switch (e) + { + case OperationFailed: + case InvalidApiKey: + case InvalidSessionKey: + // NOTE will never be received during the LoginDialog stage + // since that happens before this slot is registered with + // QMetaObject in App::App(). Neat :) + QMetaObject::invokeMethod( qApp, "onWsError", Q_ARG( lastfm::ws::Error, e ) ); + default: + throw ParseError(e); + } + } + + // bit dodgy, but prolly for the best + reply->deleteLater(); +} + + +QNetworkAccessManager* +lastfm::nam() +{ + if (!::nam) ::nam = new NetworkAccessManager( qApp ); + return ::nam; +} + + +void +lastfm::setNetworkAccessManager( QNetworkAccessManager* nam ) +{ + delete ::nam; + ::nam = nam; + nam->setParent( qApp ); // ensure it isn't deleted out from under us +} + + +/** This useful function, fromHttpDate, comes from QNetworkHeadersPrivate + * in qnetworkrequest.cpp. Qt copyright and license apply. */ +static QDateTime QByteArrayToHttpDate(const QByteArray &value) +{ + // HTTP dates have three possible formats: + // RFC 1123/822 - ddd, dd MMM yyyy hh:mm:ss "GMT" + // RFC 850 - dddd, dd-MMM-yy hh:mm:ss "GMT" + // ANSI C's asctime - ddd MMM d hh:mm:ss yyyy + // We only handle them exactly. If they deviate, we bail out. + + int pos = value.indexOf(','); + QDateTime dt; + if (pos == -1) { + // no comma -> asctime(3) format + dt = QDateTime::fromString(QString::fromLatin1(value), Qt::TextDate); + } else { + // eat the weekday, the comma and the space following it + QString sansWeekday = QString::fromLatin1(value.constData() + pos + 2); + + QLocale c = QLocale::c(); + if (pos == 3) + // must be RFC 1123 date + dt = c.toDateTime(sansWeekday, QLatin1String("dd MMM yyyy hh:mm:ss 'GMT")); + else + // must be RFC 850 date + dt = c.toDateTime(sansWeekday, QLatin1String("dd-MMM-yy hh:mm:ss 'GMT'")); + } + + if (dt.isValid()) + dt.setTimeSpec(Qt::UTC); + return dt; +} + + +QDateTime +lastfm::ws::expires( QNetworkReply* reply ) +{ + return QByteArrayToHttpDate( reply->rawHeader( "Expires" ) ); +} + + +namespace lastfm +{ + namespace ws + { + QString SessionKey; + QString Username; + + /** we leave these unset as you can't use the webservices without them + * so lets make the programmer aware of it during testing by crashing */ + const char* SharedSecret; + const char* ApiKey; + + /** if this is found set to "" we conjure ourselves a suitable one */ + const char* UserAgent = 0; + } +} + + +QDebug operator<<( QDebug, lastfm::ws::Error ); |