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 | |
parent | 97e08a249e8324bf2525fc96961fcfd5212d8da6 (diff) |
Added last.fm scrobbling.
40 files changed, 3178 insertions, 22 deletions
@@ -10,6 +10,7 @@ configure-stamp build-stamp debian/* libQtSpotify/spotify_key.h +liblastfm/lastfm_key.h # General callgrind.out.* diff --git a/MeeSpot.pro b/MeeSpot.pro index 911b9f7..c8309fd 100644 --- a/MeeSpot.pro +++ b/MeeSpot.pro @@ -25,7 +25,8 @@ CONFIG += qmsystem2 # The .cpp file which was generated for your project. Feel free to hack it. SOURCES += main.cpp \ - src/hardwarekeyshandler.cpp + src/hardwarekeyshandler.cpp \ + src/lastfmscrobbler.cpp OTHER_FILES += \ qml/MainPage.qml \ @@ -82,7 +83,8 @@ OTHER_FILES += \ qml/ToplistPage.qml \ qml/MySelectionDialog.qml \ qml/MyCommonDialog.qml \ - qml/Scrollbar.qml + qml/Scrollbar.qml \ + qml/LastfmLoginSheet.qml RESOURCES += \ res.qrc @@ -95,6 +97,7 @@ INCLUDEPATH += /usr/include/applauncherd LIBS += -lmdeclarativecache include(libQtSpotify/libQtSpotify.pri) +include(liblastfm/liblastfm.pri) # enable booster CONFIG += qdeclarative-boostable @@ -102,7 +105,11 @@ QMAKE_CXXFLAGS += -fPIC -fvisibility=hidden -fvisibility-inlines-hidden QMAKE_LFLAGS += -pie -rdynamic -Wl,-rpath,/opt/MeeSpot/lib HEADERS += \ - src/hardwarekeyshandler.h + src/hardwarekeyshandler.h \ + src/lastfmscrobbler.h + + + @@ -12,7 +12,9 @@ libspotify/ libspotify.so libspotify.so.9 -You also need your own libpotify API key to be able to compile and run the program +---- + +You also need your own libspotify API key to be able to compile and run the program (see https://developer.spotify.com/en/libspotify/application-key/) Create a file spotify_key.h inside libQtSpotify and copy the provided key inside it using the following format: @@ -25,6 +27,23 @@ using the following format: #endif // SPOTIFY_KEY_H +---- + +In addition to the libspotify API key, you also need to get a last.fm API key +(see http://www.last.fm/api/account) +Create a file lastfm_key.h inside liblastfm and copy the provided API key and shared +secret inside it using the following format: + + #ifndef LASTFM_KEY_H + #define LASTFM_KEY_H + + const char *g_lastfmAPIKey = "<api_key>"; + const char *g_lastfmSecret = "<shared_secret>"; + + #endif // LASTFM_KEY_H + +---- + To compile the project, use the Qt SDK (version 1.1 or higher) with the Harmattan component installed from the Qt SDK maintenance tool. ---> Open the project in Qt Creator, compile and deploy on the device!
\ No newline at end of file +--> Open the project in Qt Creator, compile and deploy on the device! diff --git a/libQtSpotify/qspotifysession.cpp b/libQtSpotify/qspotifysession.cpp index 3002cd4..3bca818 100644 --- a/libQtSpotify/qspotifysession.cpp +++ b/libQtSpotify/qspotifysession.cpp @@ -435,6 +435,7 @@ QSpotifySession::QSpotifySession() , m_currentTrack(0) , m_isPlaying(false) , m_currentTrackPosition(0) + , m_currentTrackPlayedDuration(0) , m_shuffle(false) , m_repeat(false) { @@ -595,6 +596,7 @@ bool QSpotifySession::event(QEvent *e) // Track progressed QSpotifyTrackProgressEvent *ev = static_cast<QSpotifyTrackProgressEvent *>(e); m_currentTrackPosition += ev->delta(); + m_currentTrackPlayedDuration += ev->delta(); emit currentTrackPositionChanged(); e->accept(); return true; @@ -815,6 +817,7 @@ void QSpotifySession::play(QSpotifyTrack *track) } m_currentTrack = track; m_currentTrackPosition = 0; + m_currentTrackPlayedDuration = 0; emit currentTrackChanged(); emit currentTrackPositionChanged(); @@ -861,6 +864,7 @@ void QSpotifySession::stop(bool dontEmitSignals) m_isPlaying = false; m_currentTrack = 0; m_currentTrackPosition = 0; + m_currentTrackPlayedDuration = 0; if (!dontEmitSignals) { emit isPlayingChanged(); diff --git a/libQtSpotify/qspotifysession.h b/libQtSpotify/qspotifysession.h index b789263..d237827 100644 --- a/libQtSpotify/qspotifysession.h +++ b/libQtSpotify/qspotifysession.h @@ -5,22 +5,22 @@ ** Contact: Yoann Lopes (yoann.lopes@nokia.com) ** ** This file is part of the MeeSpot project. -** +** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: -** +** ** Redistributions of source code must retain the above copyright notice, ** this list of conditions and the following disclaimer. -** +** ** Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. -** +** ** Neither the name of Nokia Corporation and its Subsidiary(-ies) nor the names of its ** contributors may be used to endorse or promote products derived from ** this software without specific prior written permission. -** +** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS @@ -142,6 +142,7 @@ public: QSpotifyTrack *currentTrack() const { return m_currentTrack; } bool hasCurrentTrack() const { return m_currentTrack != 0; } int currentTrackPosition() const { return m_currentTrackPosition; } + int currentTrackPlayedDuration() const { return m_currentTrackPlayedDuration; } StreamingQuality streamingQuality() const { return m_streamingQuality; } void setStreamingQuality(StreamingQuality q); @@ -260,6 +261,7 @@ private: QSpotifyTrack *m_currentTrack; bool m_isPlaying; int m_currentTrackPosition; + int m_currentTrackPlayedDuration; bool m_shuffle; bool m_repeat; diff --git a/liblastfm/Album.h b/liblastfm/Album.h new file mode 100755 index 0000000..2b5c1a9 --- /dev/null +++ b/liblastfm/Album.h @@ -0,0 +1,55 @@ +/* + 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/>. +*/ +#ifndef LASTFM_ALBUM_H +#define LASTFM_ALBUM_H + +#include "Artist.h" +#include <QString> +#include <QUrl> + +namespace lastfm +{ + class Album + { + Artist m_artist; + QString m_title; + + public: + Album() + {} + + + Album( Artist artist, QString title ) : m_artist( artist ), m_title( title ) + {} + + bool operator==( const Album& that ) const { return m_title == that.m_title && m_artist == that.m_artist; } + bool operator!=( const Album& that ) const { return m_title != that.m_title || m_artist != that.m_artist; } + + operator QString() const { return title(); } + QString title() const { return m_title.isEmpty() ? "[unknown]" : m_title; } + Artist artist() const { return m_artist; } + + /** artist may have been set, since we allow that in the ctor, but should we handle untitled albums? */ + bool isNull() const { return m_title.isEmpty(); } + + }; +} + +#endif //LASTFM_ALBUM_H diff --git a/liblastfm/Artist.h b/liblastfm/Artist.h new file mode 100755 index 0000000..3f2cee2 --- /dev/null +++ b/liblastfm/Artist.h @@ -0,0 +1,62 @@ +/* + 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/>. +*/ +#ifndef LASTFM_ARTIST_H +#define LASTFM_ARTIST_H + +#include "global.h" +#include <QMap> +#include <QString> +#include <QUrl> + +namespace lastfm +{ + class Artist + { + QString m_name; + QList<QUrl> m_images; + + public: + Artist() + {} + + Artist( const QString& name ) : m_name( name ) + {} + + /** will be QUrl() unless you got this back from a getInfo or something call */ + QUrl imageUrl( ImageSize size = Large ) const { return m_images.value( size ); } + + bool isNull() const { return m_name.isEmpty(); } + + bool operator==( const Artist& that ) const { return m_name == that.m_name; } + bool operator!=( const Artist& that ) const { return m_name != that.m_name; } + + operator QString() const + { + /** if no artist name is set, return the musicbrainz unknown identifier + * in case some part of the GUI tries to display it anyway. Note isNull + * returns false still. So you should have queried that! */ + return m_name.isEmpty() ? "[unknown]" : m_name; + } + QString name() const { return QString(*this); } + + }; +} + +#endif diff --git a/liblastfm/Audioscrobbler.cpp b/liblastfm/Audioscrobbler.cpp new file mode 100755 index 0000000..c00ee8b --- /dev/null +++ b/liblastfm/Audioscrobbler.cpp @@ -0,0 +1,142 @@ +/* + 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 "Audioscrobbler.h" +#include "NowPlaying.h" +#include "ScrobbleCache.h" +#include "ScrobblerSubmission.h" +#include "ws.h" + + +namespace lastfm +{ + struct AudioscrobblerPrivate + { + AudioscrobblerPrivate() + : cache( ws::Username ) + {} + + ~AudioscrobblerPrivate() + { + delete np; + delete submitter; + } + + QPointer<NowPlaying> np; + QPointer<ScrobblerSubmission> submitter; + ScrobbleCache cache; + }; +} + + +lastfm::Audioscrobbler::Audioscrobbler() + : d( new AudioscrobblerPrivate() ) +{ + d->np = new NowPlaying(); + connect(d->np, SIGNAL(done(QByteArray)), SLOT(onNowPlayingReturn(QByteArray)), Qt::QueuedConnection); + d->submitter = new ScrobblerSubmission; + connect(d->submitter, SIGNAL(done(QByteArray)), SLOT(onSubmissionReturn(QByteArray)), Qt::QueuedConnection); +} + + +lastfm::Audioscrobbler::~Audioscrobbler() +{ + delete d; +} + + +void +lastfm::Audioscrobbler::nowPlaying( const Track& track ) +{ + d->np->submit( track ); +} + + +void +lastfm::Audioscrobbler::cache( const Track& track ) +{ + d->cache.add( track ); +} + + +void +lastfm::Audioscrobbler::cache( const QList<Track>& tracks ) +{ + d->cache.add( tracks ); +} + + +void +lastfm::Audioscrobbler::submit() +{ + d->submitter->setTracks( d->cache.tracks() ); + d->submitter->submitNextBatch(); + + if (d->submitter->isActive()) + emit status( Scrobbling ); +} + +void +lastfm::Audioscrobbler::onNowPlayingReturn( const QByteArray& result ) +{ + if (result.isEmpty()) + return; + + lastfm::XmlQuery xml(result); + QString status = xml.attribute("status"); + + if (status == "ok") + { + d->np->reset(); + } + else + { + int errorCode = xml["error"].attribute("code").toInt(); + if (errorCode == lastfm::ws::TryAgainLater || errorCode == lastfm::ws::RateLimitExceeded) + d->np->retry(); + } +} + + +void +lastfm::Audioscrobbler::onSubmissionReturn( const QByteArray& result ) +{ + if (result.isEmpty()) + return; + + lastfm::XmlQuery xml(result); + QString statusCode = xml.attribute("status"); + + if (statusCode == "ok") + { + d->cache.remove(d->submitter->batch()); + d->submitter->submitNextBatch(); + + if (d->submitter->batch().isEmpty()) + { + emit status(Audioscrobbler::TracksScrobbled); + } + } + else + { + int errorCode = xml["error"].attribute("code").toInt(); + if (errorCode == lastfm::ws::TryAgainLater || errorCode == lastfm::ws::RateLimitExceeded) + d->submitter->retry(); + } +} diff --git a/liblastfm/Audioscrobbler.h b/liblastfm/Audioscrobbler.h new file mode 100755 index 0000000..2da58a5 --- /dev/null +++ b/liblastfm/Audioscrobbler.h @@ -0,0 +1,107 @@ +/* + 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/>. +*/ +#ifndef LASTFM_AUDIOSCROBBLER_H +#define LASTFM_AUDIOSCROBBLER_H + +#include "global.h" +#include <QByteArray> +#include <QList> +#include <QString> +#include <QObject> +#include <QVariant> + +namespace lastfm +{ + /** @author Max Howell <max@last.fm> + * An implementation of the Audioscrobbler Realtime Submissions Protocol + * version 1.2.1 for a single Last.fm user + * http://www.audioscrobbler.net/development/protocol/ + */ + class Audioscrobbler : public QObject + { + Q_OBJECT + + public: + /** You will need to do QCoreApplication::setVersion and + * QCoreApplication::setApplicationName for this to work, also you will + * need to have set all the keys in the Ws namespace in WsKeys.h */ + Audioscrobbler(); + ~Audioscrobbler(); + + public slots: + /** will ask Last.fm to update the now playing information for the + * authenticated user */ + void nowPlaying( const Track& ); + /** will cache the track, but we won't submit it until you call submit() */ + void cache( const Track& ); + /** will submit the submission cache for this user */ + void submit(); + + public: + void cache( const QList<Track>& ); + + public: + enum Status + { + Connecting, + Handshaken, + Scrobbling, + TracksScrobbled, + + StatusMax + }; + + enum Error + { + /** the following will show via the status signal, the scrobbler will + * not submit this session (np too), however caching will continue */ + ErrorBadSession = StatusMax, + ErrorBannedClientVersion, + ErrorInvalidSessionKey, + ErrorBadTime, + ErrorThreeHardFailures, + ErrorBusy, + ErrorOffline + }; + + signals: + /** the controller should show status in an appropriate manner */ + void status( int code ); + + private slots: + void onNowPlayingReturn( const QByteArray& ); + void onSubmissionReturn( const QByteArray& ); + + private: + class AudioscrobblerPrivate* d; + }; +} + + +static inline QDebug operator<<( QDebug d, lastfm::Audioscrobbler::Status status ) +{ + return d << lastfm::qMetaEnumString<lastfm::Audioscrobbler>( status, "Status" ); +} +static inline QDebug operator<<( QDebug d, lastfm::Audioscrobbler::Error error ) +{ + return d << lastfm::qMetaEnumString<lastfm::Audioscrobbler>( error, "Status" ); +} + +#endif diff --git a/liblastfm/NetworkAccessManager.cpp b/liblastfm/NetworkAccessManager.cpp new file mode 100755 index 0000000..65ffc73 --- /dev/null +++ b/liblastfm/NetworkAccessManager.cpp @@ -0,0 +1,70 @@ +/* + 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 "NetworkAccessManager.h" +//#include "InternetConnectionMonitor.h" +#include "ws.h" +#include "misc.h" +#include <QCoreApplication> +#include <QNetworkRequest> + + +namespace lastfm +{ + QByteArray UserAgent; +} + + +lastfm::NetworkAccessManager::NetworkAccessManager( QObject* parent ) + : QNetworkAccessManager( parent ) +{ + // can't be done in above init, as applicationName() won't be set + if (lastfm::UserAgent.isEmpty()) + { + QByteArray name = QCoreApplication::applicationName().toUtf8(); + QByteArray version = QCoreApplication::applicationVersion().toUtf8(); + if (version.size()) version.prepend( ' ' ); + lastfm::UserAgent = name + version; + } +} + + +lastfm::NetworkAccessManager::~NetworkAccessManager() +{ +} + + +QNetworkProxy +lastfm::NetworkAccessManager::proxy( const QNetworkRequest& request ) +{ + Q_UNUSED( request ); + + return QNetworkProxy::applicationProxy(); +} + + +QNetworkReply* +lastfm::NetworkAccessManager::createRequest( Operation op, const QNetworkRequest& request_, QIODevice* outgoingData ) +{ + QNetworkRequest request = request_; + + request.setRawHeader( "User-Agent", lastfm::UserAgent ); + + return QNetworkAccessManager::createRequest( op, request, outgoingData ); +} diff --git a/liblastfm/NetworkAccessManager.h b/liblastfm/NetworkAccessManager.h new file mode 100755 index 0000000..098b4a5 --- /dev/null +++ b/liblastfm/NetworkAccessManager.h @@ -0,0 +1,58 @@ +/* + 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/>. +*/ +#ifndef LASTFM_WS_ACCESS_MANAGER_H +#define LASTFM_WS_ACCESS_MANAGER_H + +#include "global.h" +#include <QtNetwork/QNetworkAccessManager> +#include <QNetworkRequest> +#include <QNetworkProxy> + + +namespace lastfm { + +/** Sets useragent and proxy. Auto detecting the proxy where possible. */ +class NetworkAccessManager : public QNetworkAccessManager +{ + Q_OBJECT + +public: + NetworkAccessManager( QObject *parent = 0 ); + ~NetworkAccessManager(); + + /** PAC allows different proxy configurations depending on the request + * URL and even UserAgent! Thus we allow you to pass that in, we + * automatically configure the proxy for every request through + * WsAccessManager */ + QNetworkProxy proxy( const QNetworkRequest& = QNetworkRequest() ); + +protected: + virtual QNetworkReply* createRequest( Operation, const QNetworkRequest&, QIODevice* outgoingdata = 0 ); + +private: + /** this function calls QNetworkAccessManager::setProxy, and thus + * configures the proxy correctly for the next request created by + * createRequest. This is necessary due */ + void applyProxy( const QNetworkRequest& ); +}; + +} //namespace lastfm + +#endif diff --git a/liblastfm/NowPlaying.cpp b/liblastfm/NowPlaying.cpp new file mode 100755 index 0000000..5302e92 --- /dev/null +++ b/liblastfm/NowPlaying.cpp @@ -0,0 +1,69 @@ +/* + 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 "NowPlaying.h" +#include "Track.h" +#include "ws.h" +#include <QTimer> + + +NowPlaying::NowPlaying() +{ + // we wait 5 seconds to prevent the server panicking when people skip a lot + // tracks in succession + m_timer = new QTimer( this ); + m_timer->setSingleShot( true ); + connect( m_timer, SIGNAL(timeout()), SLOT(request()) ); +} + + +void +NowPlaying::reset() +{ + m_timer->stop(); + m_data.clear(); +} + + +void +NowPlaying::submit( const lastfm::Track& track ) +{ + if (track.isNull()) + return; + + m_data["method"] = "track.updateNowPlaying"; + m_data["track"] = track.title(); + m_data["artist"] = track.artist(); + m_data["album"] = track.album(); + m_data["trackNumber"] = QByteArray::number(track.trackNumber()); + m_data["duration"] = QByteArray::number(track.duration()); + + // m_time is initialised to midnight, so a bug exists that if the app is + // started after 00:00 and before 00:04 we trigger via the timer. But meh! + uint ms = m_delay.elapsed(); + + if (ms < 10000) { + m_timer->setInterval( 10000 - ms ); + m_timer->start(); + } + else { + m_delay.restart(); + request(); + } +} diff --git a/liblastfm/NowPlaying.h b/liblastfm/NowPlaying.h new file mode 100755 index 0000000..8a440e6 --- /dev/null +++ b/liblastfm/NowPlaying.h @@ -0,0 +1,41 @@ +/* + 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/>. +*/ +#ifndef LASTFM_NOW_PLAYING_H +#define LASTFM_NOW_PLAYING_H + +#include "global.h" +#include "ScrobblerHttp.h" +#include <QTime> + + +class NowPlaying : public ScrobblerPostHttp +{ + class QTimer* m_timer; + QTime m_delay; + +public: + NowPlaying(); + void submit( const lastfm::Track& ); + void reset(); + + using ScrobblerPostHttp::request; +}; + +#endif diff --git a/liblastfm/Scrobble.cpp b/liblastfm/Scrobble.cpp new file mode 100755 index 0000000..53b48ee --- /dev/null +++ b/liblastfm/Scrobble.cpp @@ -0,0 +1,76 @@ +/* + 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 "Scrobble.h" +#include "ScrobblePoint.h" +#include <QStringList> +using lastfm::Scrobble; + + +QByteArray +Scrobble::sourceString() const +{ + switch (d->source) + { + case LastFmRadio: return "L" + d->extras["trackauth"].toAscii(); + case Player: return "P" + d->extras["playerId"].toUtf8(); + case MediaDevice: return "P" + d->extras["mediaDeviceId"].toUtf8(); + case NonPersonalisedBroadcast: return "R"; + case PersonalisedRecommendation: return "E"; + default: return "U"; + } +} + + +bool +Scrobble::isValid( Invalidity* v ) const +{ + #define TEST( test, x ) \ + if (test) { \ + if (v) *v = x; \ + return false; \ + } + + TEST( duration() < ScrobblePoint::kScrobbleMinLength, TooShort ); + + // Radio tracks above preview length always scrobble + if (source() == LastFmRadio) + return true; + + TEST( !timestamp().isValid(), NoTimestamp ); + + // actual spam prevention is something like 12 hours, but we are only + // trying to weed out obviously bad data, server side criteria for + // "the future" may change, so we should let the server decide, not us + TEST( timestamp() > QDateTime::currentDateTime().addMonths( 1 ), FromTheFuture ); + + TEST( timestamp() < QDateTime::fromString( "2003-01-01", Qt::ISODate ), FromTheDistantPast ); + + // Check if any required fields are empty + TEST( d->artist.isEmpty(), ArtistNameMissing ); + TEST( d->title.isEmpty(), TrackNameMissing ); + + TEST( (QStringList() << "unknown artist" + << "unknown" + << "[unknown]" + << "[unknown artist]").contains( d->artist.toLower() ), + ArtistInvalid ); + + return true; +} diff --git a/liblastfm/Scrobble.h b/liblastfm/Scrobble.h new file mode 100755 index 0000000..67d0b4a --- /dev/null +++ b/liblastfm/Scrobble.h @@ -0,0 +1,63 @@ +/* + 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/>. +*/ +#ifndef LASTFM_SCROBBLE_H +#define LASTFM_SCROBBLE_H + +#include "Track.h" + +namespace lastfm +{ + struct Scrobble : lastfm::Track + { + Scrobble() + {} + + Scrobble( const lastfm::Track& that ) : Track( that ) + {} + + QByteArray sourceString() const; + + QByteArray ratingCharacter() const + { + return d->extras["rating"].toAscii(); + } + + bool isLoved() const { return ratingCharacter() == QChar('L'); } + bool isBanned() const { return ratingCharacter() == QChar('B'); } + bool isSkipped() const { return ratingCharacter() == QChar('S'); } + + /** if isValid() returns false, we will not scrobble the track */ + enum Invalidity + { + TooShort, + ArtistNameMissing, + TrackNameMissing, + ArtistInvalid, + NoTimestamp, + FromTheFuture, + FromTheDistantPast + }; + + /** @returns true if the server is unlikely to reject this scrobble */ + bool isValid( Invalidity* = 0 ) const; + }; +} + +#endif diff --git a/liblastfm/ScrobbleCache.cpp b/liblastfm/ScrobbleCache.cpp new file mode 100755 index 0000000..d8b35ec --- /dev/null +++ b/liblastfm/ScrobbleCache.cpp @@ -0,0 +1,136 @@ +/* + 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 "ScrobbleCache.h" +#include "misc.h" +#include <QCoreApplication> +#include <QFile> +#include <QDomElement> +#include <QDomDocument> +#if LASTFM_VERSION >= 0x00010000 +using lastfm::ScrobbleCache; +#endif + + +ScrobbleCache::ScrobbleCache( const QString& username ) +{ + Q_ASSERT( username.length() ); + + m_path = lastfm::dir::runtimeData().filePath( username + "_subs_cache.xml" ); + m_username = username; + + QDomDocument xml; + read( xml ); +} + + +void +ScrobbleCache::read( QDomDocument& xml ) +{ + m_tracks.clear(); + + QFile file( m_path ); + file.open( QFile::Text | QFile::ReadOnly ); + QTextStream stream( &file ); + stream.setCodec( "UTF-8" ); + + xml.setContent( stream.readAll() ); + + for (QDomNode n = xml.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) + if (n.nodeName() == "track") + m_tracks += Track( n.toElement() ); +} + + +void +ScrobbleCache::write() +{ + if (m_tracks.isEmpty()) + { + QFile::remove( m_path ); + } + else { + QDomDocument xml; + QDomElement e = xml.createElement( "submissions" ); + e.setAttribute( "product", QCoreApplication::applicationName() ); + e.setAttribute( "version", "2" ); + + foreach (Track i, m_tracks) + e.appendChild( i.toDomElement( xml ) ); + + xml.appendChild( e ); + + QFileInfo(m_path).dir().mkpath("."); + + QFile file( m_path ); + file.open( QIODevice::WriteOnly | QIODevice::Text ); + + QTextStream stream( &file ); + stream.setCodec( "UTF-8" ); + stream << "<?xml version='1.0' encoding='utf-8'?>\n"; + stream << xml.toString( 2 ); + } +} + + +void +ScrobbleCache::add( const Scrobble& track ) +{ + add( QList<Track>() << track ); +} + + +void +ScrobbleCache::add( const QList<Track>& tracks ) +{ + foreach (const Track& track, tracks) + { + Scrobble::Invalidity invalidity; + + if (!Scrobble(track).isValid( &invalidity )) + { + qWarning() << invalidity; + } + else if (track.isNull()) + qDebug() << "Will not cache an empty track"; + + else if (!m_tracks.contains( track )) + m_tracks += track; + } + write(); +} + + +int +ScrobbleCache::remove( const QList<Track>& toremove ) +{ + QMutableListIterator<Track> i( m_tracks ); + while (i.hasNext()) { + Track t = i.next(); + for (int x = 0; x < toremove.count(); ++x) + if (toremove[x] == t) + i.remove(); + } + + write(); + + // yes we return # remaining, rather # removed, but this is an internal + // function and the behaviour is documented so it's alright imo --mxcl + return m_tracks.count(); +} diff --git a/liblastfm/ScrobbleCache.h b/liblastfm/ScrobbleCache.h new file mode 100755 index 0000000..e9716b1 --- /dev/null +++ b/liblastfm/ScrobbleCache.h @@ -0,0 +1,74 @@ +/* + 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/>. +*/ +#ifndef LASTFM_SCROBBLE_CACHE_H +#define LASTFM_SCROBBLE_CACHE_H + +#include "Scrobble.h" +#include <QList> +#include <QString> + + +#if LASTFM_VERSION >= 0x00010000 +namespace lastfm { +#else +using lastfm::Scrobble; +using lastfm::Track; +#endif + +/** absolutely not thread-safe */ +class ScrobbleCache +{ + QString m_username; + + void write(); /// writes m_tracks to m_path + +protected: + ScrobbleCache() + {} + + QString m_path; + QList<Track> m_tracks; + + void read( QDomDocument& xml ); /// reads from m_path into m_tracks + +public: + explicit ScrobbleCache( const QString& username ); + + /** note this is unique for Track::sameAs() and equal timestamps + * obviously playcounts will not be increased for the same timestamp */ + void add( const Scrobble& ); + void add( const QList<Track>& ); + + /** returns the number of tracks left in the queue */ + int remove( const QList<Track>& ); + + QList<Track> tracks() const { return m_tracks; } + QString path() const { return m_path; } + QString username() const { return m_username; } + +private: + bool operator==( const ScrobbleCache& ); //undefined +}; + +#if LASTFM_VERSION >= 0x00010000 +} +#endif + +#endif diff --git a/liblastfm/ScrobblePoint.h b/liblastfm/ScrobblePoint.h new file mode 100755 index 0000000..762d9f6 --- /dev/null +++ b/liblastfm/ScrobblePoint.h @@ -0,0 +1,59 @@ +/* + 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/>. +*/ +#ifndef LASTFM_SCROBBLE_POINT_H +#define LASTFM_SCROBBLE_POINT_H + +#include "global.h" +#include <QtAlgorithms> + + +class ScrobblePoint +{ + uint i; + +public: + ScrobblePoint() : i( kScrobbleTimeMax ) + {} + + /** j is in seconds, and should be 50% the duration of a track */ + explicit ScrobblePoint( uint j ) + { + // we special case 0, returning kScrobbleTimeMax because we are + // cruel and callous people + if (j == 0) --j; + + i = qBound( uint(kScrobbleMinLength), + j, + uint(kScrobbleTimeMax) ); + } + operator uint() const { return i; } + + // scrobbles can occur between these two percentages of track duration + static const uint kScrobblePointMin = 50; + static const uint kScrobblePointMax = 100; + static const uint kDefaultScrobblePoint = 50; + + // Shortest track length allowed to scrobble in seconds + static const uint kScrobbleMinLength = 31; + // Upper limit for scrobble time in seconds + static const uint kScrobbleTimeMax = 240; +}; + +#endif diff --git a/liblastfm/ScrobblerHttp.cpp b/liblastfm/ScrobblerHttp.cpp new file mode 100755 index 0000000..c9b151e --- /dev/null +++ b/liblastfm/ScrobblerHttp.cpp @@ -0,0 +1,96 @@ +/* + 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 "ScrobblerHttp.h" +#include <QTimer> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include "ws.h" + + +ScrobblerHttp::ScrobblerHttp( QObject* parent ) + : QObject( parent ) +{ + m_retry_timer = new QTimer( this ); + m_retry_timer->setSingleShot( true ); + connect( m_retry_timer, SIGNAL(timeout()), SLOT(request()) ); + resetRetryTimer(); +} + + +void +ScrobblerHttp::onRequestFinished() +{ + if (rp->error() == QNetworkReply::OperationCanceledError) + ; //we aborted it + if (rp->error()) + { + qWarning() << "ERROR!" << rp->error(); + emit done( QByteArray() ); + } + else + { + emit done( rp->readAll() ); + + // if it is running then someone called retry() in the slot connected to + // the done() signal above, so don't reset it, init + if (!m_retry_timer->isActive()) + resetRetryTimer(); + } + + rp->deleteLater(); +} + + +void +ScrobblerHttp::retry() +{ + if (!m_retry_timer->isActive()) + { + int const i = m_retry_timer->interval(); + if (i < 120 * 60 * 1000) + m_retry_timer->setInterval( i * 2 ); + } + + qDebug() << "Will retry in" << m_retry_timer->interval() / 1000 << "seconds"; + + m_retry_timer->start(); +} + + +void +ScrobblerHttp::resetRetryTimer() +{ + m_retry_timer->setInterval( 30 * 1000 ); +} + + +void +ScrobblerPostHttp::request() +{ + if (m_data.isEmpty()) + return; + + if (rp) + rp->deleteLater(); + + rp = lastfm::ws::post(m_data); + connect( rp, SIGNAL(finished()), SLOT(onRequestFinished()) ); + rp->setParent( this ); +} diff --git a/liblastfm/ScrobblerHttp.h b/liblastfm/ScrobblerHttp.h new file mode 100755 index 0000000..fcda270 --- /dev/null +++ b/liblastfm/ScrobblerHttp.h @@ -0,0 +1,74 @@ +/* + 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/>. +*/ +#ifndef SCROBBLER_HTTP_H +#define SCROBBLER_HTTP_H + +#include <QPointer> +#include <QUrl> +#include "XmlQuery.h" +class QNetworkReply; + + +/** This was a QHttp class, but then we realised QNetworkAccessManager and that + * is oodles better. So we chnaged it to use that. */ +class ScrobblerHttp : public QObject +{ + Q_OBJECT + +public: + void retry(); + bool isActive() const { return !rp.isNull(); } + +protected: + ScrobblerHttp( QObject* parent = 0 ); + +protected slots: + virtual void request() = 0; + +signals: + void done( const QByteArray& data ); + +protected: + class QTimer *m_retry_timer; + QPointer<QNetworkReply> rp; + +private slots: + void onRequestFinished(); + +private: + void resetRetryTimer(); +}; + + +class ScrobblerPostHttp : public ScrobblerHttp +{ +protected: + QMap<QString, QString> m_data; + +public: + ScrobblerPostHttp() + {} + + /** if you reimplement call the base version after setting m_data */ + virtual void request(); + +}; + +#endif diff --git a/liblastfm/ScrobblerSubmission.cpp b/liblastfm/ScrobblerSubmission.cpp new file mode 100755 index 0000000..a2ef07e --- /dev/null +++ b/liblastfm/ScrobblerSubmission.cpp @@ -0,0 +1,69 @@ +/* + 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 "ScrobblerSubmission.h" +#include "ScrobbleCache.h" +#include "Scrobble.h" + +using lastfm::Track; +using lastfm::Scrobble; + +void +ScrobblerSubmission::setTracks( const QList<Track>& tracks ) +{ + m_tracks = tracks; + // submit in chronological order + qSort( m_tracks.begin(), m_tracks.end() ); +} + + +void +ScrobblerSubmission::submitNextBatch() +{ + if (isActive()) + // the tracks cannot be submitted at this time + // if a parent Scrobbler instance exists, it will submit another batch + // when the current one is done + return; + + m_batch.clear(); //yep before isEmpty() check + m_data.clear(); + + if (m_tracks.isEmpty()) + return; + + m_data["method"] = "track.scrobble"; + + for (int i = 0; i < 50 && !m_tracks.isEmpty(); ++i) + { + Scrobble s = m_tracks.takeFirst(); + + QByteArray const N = QByteArray::number( i ); + m_data["timestamp[" + N + "]"] = QString::number(s.timestamp().toTime_t()); + m_data["album[" + N + "]"] = s.album(); + m_data["track[" + N + "]"] = s.title(); + m_data["artist[" + N + "]"] = s.artist(); + m_data["duration[" + N + "]"] = QString::number(s.duration()); + m_data["trackNumber[" + N + "]"] = QString::number(s.trackNumber()); + + m_batch += s; + } + + request(); +}; diff --git a/liblastfm/ScrobblerSubmission.h b/liblastfm/ScrobblerSubmission.h new file mode 100755 index 0000000..0a8803d --- /dev/null +++ b/liblastfm/ScrobblerSubmission.h @@ -0,0 +1,48 @@ +/* + 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/>. +*/ +#ifndef LASTFM_SCROBBLER_SUBMISSION_H +#define LASTFM_SCROBBLER_SUBMISSION_H + +#include "ScrobblerHttp.h" +#include "Track.h" +#include <QList> + +class ScrobblerSubmission : public ScrobblerPostHttp +{ + QList<lastfm::Track> m_tracks; + QList<lastfm::Track> m_batch; + +public: + /** tracks will be submitted in batches of 50 */ + void setTracks( const QList<lastfm::Track>& ); + /** submits a batch, if we are already submitting, does nothing */ + void submitNextBatch(); + /** the batch that is being submitted currently */ + QList<lastfm::Track> batch() const { return m_batch; } + /** tracks that have not yet been removed due to an OK from Last.fm */ + QList<lastfm::Track> unsubmittedTracks() const { return m_tracks; } + + virtual void request() + { + if (!isActive()) ScrobblerPostHttp::request(); + } +}; + +#endif diff --git a/liblastfm/Track.cpp b/liblastfm/Track.cpp new file mode 100755 index 0000000..e4aec52 --- /dev/null +++ b/liblastfm/Track.cpp @@ -0,0 +1,147 @@ +/* + 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 "Track.h" +//#include "User.h" +//#include "../core/UrlBuilder.h" +#include "XmlQuery.h" +#include "ws.h" +#include <QFileInfo> +#include <QStringList> + + +lastfm::Track::Track() +{ + d = new TrackData; + d->null = true; +} + + +lastfm::Track::Track( const QDomElement& e ) +{ + d = new TrackData; + + if (e.isNull()) { d->null = true; return; } + + d->artist = e.namedItem( "artist" ).toElement().text(); + d->album = e.namedItem( "album" ).toElement().text(); + d->title = e.namedItem( "track" ).toElement().text(); + d->trackNumber = 0; + d->duration = e.namedItem( "duration" ).toElement().text().toInt(); + d->url = e.namedItem( "url" ).toElement().text(); + d->rating = e.namedItem( "rating" ).toElement().text().toUInt(); + d->source = e.namedItem( "source" ).toElement().text().toInt(); //defaults to 0, or lastfm::Track::Unknown + d->time = QDateTime::fromTime_t( e.namedItem( "timestamp" ).toElement().text().toUInt() ); + + QDomNodeList nodes = e.namedItem( "extras" ).childNodes(); + for (int i = 0; i < nodes.count(); ++i) + { + QDomNode n = nodes.at(i); + QString key = n.nodeName(); + d->extras[key] = n.toElement().text(); + } +} + + +QDomElement +lastfm::Track::toDomElement( QDomDocument& xml ) const +{ + QDomElement item = xml.createElement( "track" ); + + #define makeElement( tagname, getter ) { \ + QString v = getter; \ + if (!v.isEmpty()) \ + { \ + QDomElement e = xml.createElement( tagname ); \ + e.appendChild( xml.createTextNode( v ) ); \ + item.appendChild( e ); \ + } \ + } + + makeElement( "artist", d->artist ); + makeElement( "album", d->album ); + makeElement( "track", d->title ); + makeElement( "duration", QString::number( d->duration ) ); + makeElement( "timestamp", QString::number( d->time.toTime_t() ) ); + makeElement( "url", d->url.toString() ); + makeElement( "source", QString::number( d->source ) ); + makeElement( "rating", QString::number(d->rating) ); + makeElement( "fpId", QString::number(d->fpid) ); + + QDomElement extras = xml.createElement( "extras" ); + QMapIterator<QString, QString> i( d->extras ); + while (i.hasNext()) { + QDomElement e = xml.createElement( i.next().key() ); + e.appendChild( xml.createTextNode( i.value() ) ); + extras.appendChild( e ); + } + item.appendChild( extras ); + + return item; +} + + +QString +lastfm::Track::toString( const QChar& separator ) const +{ + if ( d->artist.isEmpty() ) + { + if ( d->title.isEmpty() ) + return QFileInfo( d->url.path() ).fileName(); + else + return d->title; + } + + if ( d->title.isEmpty() ) + return d->artist; + + return d->artist + ' ' + separator + ' ' + d->title; +} + + +QString //static +lastfm::Track::durationString( int const duration ) +{ + QTime t = QTime().addSecs( duration ); + if (duration < 60*60) + return t.toString( "m:ss" ); + else + return t.toString( "hh:mm:ss" ); +} + + +QMap<QString, QString> +lastfm::Track::params( const QString& method) const +{ + QMap<QString, QString> map; + map["method"] = "track."+method; + map["artist"] = d->artist; + map["track"] = d->title; + + return map; +} + + +lastfm::Track +lastfm::Track::clone() const +{ + Track copy( *this ); + copy.d.detach(); + return copy; +} diff --git a/liblastfm/Track.h b/liblastfm/Track.h new file mode 100755 index 0000000..e85ae69 --- /dev/null +++ b/liblastfm/Track.h @@ -0,0 +1,219 @@ +/* + 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/>. +*/ +#ifndef LASTFM_TRACK_H +#define LASTFM_TRACK_H + +#include "Album.h" +#include <QDateTime> +#include <QDomElement> +#include <QExplicitlySharedDataPointer> +#include <QString> +#include <QMap> +#include <QUrl> + + +namespace lastfm { + + +struct TrackData : QSharedData +{ + TrackData(); + + QString artist; + QString album; + QString title; + uint trackNumber; + uint duration; + short source; + short rating; + uint fpid; + QUrl url; + QDateTime time; /// the time the track was started at + + //FIXME I hate this, but is used for radio trackauth etc. + QMap<QString,QString> extras; + + bool null; +}; + + + +/** Our track type. It's quite good, you may want to use it as your track type + * in general. It is explicitly shared. Which means when you make a copy, they + * both point to the same data still. This is like Qt's implicitly shared + * classes, eg. QString, however if you mod a copy of a QString, the copy + * detaches first, so then you have two copies. Our Track object doesn't + * detach, which is very handy for our usage in the client, but perhaps not + * what you want. If you need a deep copy for eg. work in a thread, call + * clone(). */ +class Track +{ +public: + enum Source + { + // DO NOT UNDER ANY CIRCUMSTANCES CHANGE THE ORDER OR VALUES OF THIS ENUM! + // you will cause broken settings and b0rked scrobbler cache submissions + + Unknown = 0, + LastFmRadio, + Player, + MediaDevice, + NonPersonalisedBroadcast, // eg Shoutcast, BBC Radio 1, etc. + PersonalisedRecommendation, // eg Pandora, but not Last.fm + }; + + Track(); + explicit Track( const QDomElement& ); + + /** if you plan to use this track in a separate thread, you need to clone it + * first, otherwise nothing is thread-safe, not this creates a disconnected + * Track object, modifications to this or that will not effect that or this + */ + Track clone() const; + + /** this track and that track point to the same object, so they are the same + * in fact. This doesn't do a deep data comparison. So even if all the + * fields are the same it will return false if they aren't in fact spawned + * from the same initial Track object */ + bool operator==( const Track& that ) const + { + return this->d == that.d; + } + bool operator!=( const Track& that ) const + { + return !operator==( that ); + } + + /** only a Track() is null */ + bool isNull() const { return d->null; } + + Artist artist() const { return Artist( d->artist ); } + Album album() const { return Album( artist(), d->album ); } + QString title() const + { + /** if no title is set, return the musicbrainz unknown identifier + * in case some part of the GUI tries to display it anyway. Note isNull + * returns false still. So you should have queried this! */ + return d->title.isEmpty() ? "[unknown]" : d->title; + } + uint trackNumber() const { return d->trackNumber; } + uint duration() const { return d->duration; } /// in seconds + QUrl url() const { return d->url; } + QDateTime timestamp() const { return d->time; } + Source source() const { return (Source)d->source; } + uint fingerprintId() const { return d->fpid; } + + QString durationString() const { return durationString( d->duration ); } + static QString durationString( int seconds ); + + /** default separator is an en-dash */ + QString toString( const QChar& separator = QChar(8211) ) const; + /** the standard representation of this object as an XML node */ + QDomElement toDomElement( class QDomDocument& ) const; + + QString extra( const QString& key ) const{ return d->extras[ key ]; } + + bool operator<( const Track &that ) const + { + return this->d->time < that.d->time; + } + + operator QVariant() const { return QVariant::fromValue( *this ); } + +//////////// lastfm::Ws + +protected: + QExplicitlySharedDataPointer<TrackData> d; + QMap<QString, QString> params( const QString& method) const; + +private: + Track( TrackData* that_d ) : d( that_d ) + {} +}; + + + +/** This class allows you to change Track objects, it is easy to use: + * MutableTrack( some_track_object ).setTitle( "Arse" ); + * + * We have a separate MutableTrack class because in our usage, tracks + * only get mutated once, and then after that, very rarely. This pattern + * encourages such usage, which is generally sensible. You can feel more + * comfortable that the data hasn't accidently changed behind your back. + */ +class MutableTrack : public Track +{ +public: + MutableTrack() + { + d->null = false; + } + + /** NOTE that passing a Track() to this ctor will automatically make it non + * null. Which may not be what you want. So be careful + * Rationale: this is the most maintainable way to do it + */ + MutableTrack( const Track& that ) : Track( that ) + { + d->null = false; + } + + void setArtist( QString artist ) { d->artist = artist.trimmed(); } + void setAlbum( QString album ) { d->album = album.trimmed(); } + void setTitle( QString title ) { d->title = title.trimmed(); } + void setTrackNumber( uint n ) { d->trackNumber = n; } + void setDuration( uint duration ) { d->duration = duration; } + void setUrl( QUrl url ) { d->url = url; } + void setSource( Source s ) { d->source = s; } + + void setFingerprintId( uint id ) { d->fpid = id; } + + void stamp() { d->time = QDateTime::currentDateTime(); } + + void setExtra( const QString& key, const QString& value ) { d->extras[key] = value; } + void removeExtra( QString key ) { d->extras.remove( key ); } +}; + + +inline +TrackData::TrackData() + : trackNumber( 0 ), + duration( 0 ), + source( Track::Unknown ), + rating( 0 ), + fpid( -1 ), + null( false ) +{} + + +} //namespace lastfm + + +inline QDebug operator<<( QDebug d, const lastfm::Track& t ) +{ + return !t.isNull() + ? d << t.toString( '-' ) << t.url() + : d << "Null Track object"; +} + + +Q_DECLARE_METATYPE( lastfm::Track ); + +#endif //LASTFM_TRACK_H diff --git a/liblastfm/XmlQuery.cpp b/liblastfm/XmlQuery.cpp new file mode 100755 index 0000000..5c68aaa --- /dev/null +++ b/liblastfm/XmlQuery.cpp @@ -0,0 +1,64 @@ +/* + 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 "XmlQuery.h" +#include <QStringList> +using lastfm::XmlQuery; + + +XmlQuery::XmlQuery( const QByteArray& bytes ) +{ + domdoc.setContent(bytes); + e = domdoc.documentElement(); +} + + +XmlQuery +XmlQuery::operator[]( const QString& name ) const +{ + QStringList parts = name.split( ' ' ); + if (parts.size() >= 2) + { + QString tagName = parts[0]; + parts = parts[1].split( '=' ); + QString attributeName = parts.value( 0 ); + QString attributeValue = parts.value( 1 ); + + foreach (XmlQuery e, children( tagName )) + if (e.e.attribute( attributeName ) == attributeValue) + return e; + } + XmlQuery xq( e.firstChildElement( name ), name.toUtf8().data() ); + xq.domdoc = this->domdoc; + return xq; +} + + +QList<XmlQuery> +XmlQuery::children( const QString& named ) const +{ + QList<XmlQuery> elements; + QDomNodeList nodes = e.elementsByTagName( named ); + for (int x = 0; x < nodes.count(); ++x) { + XmlQuery xq( nodes.at( x ).toElement() ); + xq.domdoc = this->domdoc; + elements += xq; + } + return elements; +} diff --git a/liblastfm/XmlQuery.h b/liblastfm/XmlQuery.h new file mode 100755 index 0000000..9afd30d --- /dev/null +++ b/liblastfm/XmlQuery.h @@ -0,0 +1,77 @@ +/* + 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/>. +*/ +#ifndef LASTFM_XMLQUERY_H +#define LASTFM_XMLQUERY_H + +#include "global.h" +#include <QDomDocument> +#include <QDomElement> + +namespace lastfm +{ + /** Qt's XmlQuery implementation is totally unimpressive, so this is a + * hack that feels like jQuery */ + class XmlQuery + { + QDomDocument domdoc; + QDomElement e; + + public: + /** we assume the bytearray is an XML document, this object will then + * represent the documentElement of that document, eg. if this is a + * Last.fm webservice response: + * + * XmlQuery xq = lastfm::ws::parse(response); + * qDebug() << xq["artist"].text() + * + * Notice the lfm node is not referenced, that is because it is the + * document-element of the XML document. + */ + XmlQuery( const QByteArray& ); + + XmlQuery( const QDomElement& e, const char* name = "" ) : e( e ) + { + if (e.isNull()) qWarning() << "Expected node absent:" << name; + } + + /** Selects a DIRECT child element, you can specify attributes like so: + * + * e["element"]["element attribute=value"].text(); + */ + XmlQuery operator[]( const QString& name ) const; + QString text() const { return e.text(); } + QString attribute(const QString &name) { return e.attribute(name); } + + /** selects all children with specified name, recursively */ + QList<XmlQuery> children( const QString& named ) const; + + operator QDomElement() const { return e; } + }; +} + +inline QDebug operator<<( QDebug d, const lastfm::XmlQuery& xq ) +{ + QString s; + QTextStream t( &s, QIODevice::WriteOnly ); + QDomElement(xq).save( t, 2 ); + return d << s; +} + +#endif diff --git a/liblastfm/global.h b/liblastfm/global.h new file mode 100755 index 0000000..e68c64b --- /dev/null +++ b/liblastfm/global.h @@ -0,0 +1,116 @@ +/* + 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/>. +*/ + +#ifndef LASTFM_GLOBAL_H +#define LASTFM_GLOBAL_H + +#define LASTFM_VERSION 0x00000303 +#define LASTFM_VERSION_STRING "0.3.3" +#define LASTFM_MAJOR_VERSION 0 +#define LASTFM_MINOR_VERSION 3 +#define LASTFM_PATCH_VERSION 3 + + +#include <QtGlobal> + +#include <QMetaEnum> +#include <QString> + +namespace lastfm +{ + /** http://labs.trolltech.com/blogs/2008/10/09/coding-tip-pretty-printing-enum-values + * Tips for making this take a single parameter welcome! :) + * + * eg. lastfm::qMetaEnumString<QNetworkReply>( error, "NetworkError" ); + */ + template <typename T> static inline QString qMetaEnumString( int enum_value, const char* enum_name ) + { + QMetaObject meta = T::staticMetaObject; + for (int i=0; i < meta.enumeratorCount(); ++i) + { + QMetaEnum m = meta.enumerator(i); + if (m.name() == QLatin1String(enum_name)) + return QLatin1String(m.valueToKey(enum_value)); + } + return QString("Unknown enum value for \"%1\": %2").arg( enum_name ).arg( enum_value ); + } + + + enum ImageSize + { + Small = 0, + Medium = 1, + Large = 2, /** seemingly 174x174 */ + ExtraLarge = 3 + }; + + + //convenience + class Album; + class Artist; + class Audioscrobbler; + class AuthenticatedUser; + class Fingerprint; + class FingerprintableSource; + class FingerprintId; + class Mbid; + class MutableTrack; + class NetworkAccessManager; + class Playlist; + class User; + class RadioStation; + class Scrobble; + class Tag; + class Track; + class XmlQuery; + class Xspf; +} + + +#ifdef LASTFM_COLLAPSE_NAMESPACE +using lastfm::Album; +using lastfm::Artist; +using lastfm::Audioscrobbler; +using lastfm::AuthenticatedUser; +using lastfm::Fingerprint; +using lastfm::FingerprintId; +using lastfm::Mbid; +using lastfm::MutableTrack; +using lastfm::Playlist; +using lastfm::User; +using lastfm::RadioStation; +using lastfm::Scrobble; +using lastfm::Tag; +using lastfm::Track; +using lastfm::XmlQuery; +using lastfm::Xspf; +#endif + + +//convenience +class QDomDocument; +class QNetworkAccessManager; +class QNetworkReply; + + +//convenience for development +#include <QDebug> + +#endif //LASTFM_GLOBAL_H diff --git a/liblastfm/liblastfm.pri b/liblastfm/liblastfm.pri new file mode 100644 index 0000000..962aafe --- /dev/null +++ b/liblastfm/liblastfm.pri @@ -0,0 +1,62 @@ + +QT += network xml + +INCLUDEPATH += $$PWD + +HEADERS += \ + liblastfm/ScrobblerSubmission.h \ + liblastfm/ScrobblerHttp.h \ + liblastfm/ScrobblePoint.h \ + liblastfm/ScrobbleCache.h \ + liblastfm/Scrobble.h \ + liblastfm/NowPlaying.h \ + liblastfm/Audioscrobbler.h \ + liblastfm/global.h \ + liblastfm/Track.h \ + liblastfm/Album.h \ + liblastfm/Artist.h \ + liblastfm/ws.h \ + liblastfm/misc.h \ + liblastfm/NetworkAccessManager.h \ + liblastfm/lastfm_key.h \ + liblastfm/XmlQuery.h + +SOURCES += \ + liblastfm/ScrobblerSubmission.cpp \ + liblastfm/ScrobblerHttp.cpp \ + liblastfm/ScrobbleCache.cpp \ + liblastfm/Scrobble.cpp \ + liblastfm/NowPlaying.cpp \ + liblastfm/Audioscrobbler.cpp \ + liblastfm/Track.cpp \ + liblastfm/ws.cpp \ + liblastfm/misc.cpp \ + liblastfm/NetworkAccessManager.cpp \ + liblastfm/XmlQuery.cpp + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/liblastfm/misc.cpp b/liblastfm/misc.cpp new file mode 100755 index 0000000..8c8531d --- /dev/null +++ b/liblastfm/misc.cpp @@ -0,0 +1,47 @@ +/* + 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 "misc.h" +#include <QDir> + + +static QDir dataDotDot() +{ + return QDir("/home/user/MyDocs/.meespotconf/MeeSpot"); +} + + +QDir +lastfm::dir::runtimeData() +{ + return dataDotDot().filePath("LastFm"); +} + +QDir +lastfm::dir::logs() +{ + return runtimeData(); +} + + +QDir +lastfm::dir::cache() +{ + return runtimeData().filePath( "cache" ); +} diff --git a/liblastfm/misc.h b/liblastfm/misc.h new file mode 100755 index 0000000..476ecca --- /dev/null +++ b/liblastfm/misc.h @@ -0,0 +1,44 @@ +/* + 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/>. +*/ +#ifndef LASTFM_MISC_H +#define LASTFM_MISC_H + +#include "global.h" +#include <QCryptographicHash> +#include <QDir> +#include <QString> + +namespace lastfm +{ + namespace dir + { + QDir runtimeData(); + QDir cache(); + QDir logs(); + } + + inline QString md5( const QByteArray& src ) + { + QByteArray const digest = QCryptographicHash::hash( src, QCryptographicHash::Md5 ); + return QString::fromLatin1( digest.toHex() ).rightJustified( 32, '0' ).toLower(); + } +} + +#endif //LASTFM_MISC_H 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 ); diff --git a/liblastfm/ws.h b/liblastfm/ws.h new file mode 100755 index 0000000..6dcde1e --- /dev/null +++ b/liblastfm/ws.h @@ -0,0 +1,164 @@ +/* + 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/>. +*/ +#ifndef LASTFM_WS_H +#define LASTFM_WS_H + +#include "global.h" +#include <QDateTime> +#include <QMap> +#include <QNetworkReply> +#include <stdexcept> + + +namespace lastfm +{ + /** if you don't set one, we create our own, our own is pretty good + * for instance, it auto detects proxy settings on windows and mac + * We take ownership of the NAM, do not delete it out from underneath us! + * So don't keep any other pointers to this around in case you accidently + * call delete on them :P */ + void setNetworkAccessManager( QNetworkAccessManager* nam ); + QNetworkAccessManager* nam(); + + namespace ws + { + /** both of these are provided when you register at http://last.fm/api */ + extern const char* SharedSecret; + extern const char* ApiKey; + + /** you need to set this for scrobbling to work (for now) + * Also the AuthenticatedUser class uses it */ + extern QString Username; + + /** Some webservices require authentication. See the following + * documentation: + * http://www.last.fm/api/authentication + * http://www.last.fm/api/desktopauth + * You have to authenticate and then assign to SessionKey, liblastfm does + * not do that for you. Also we do not store this. You should store this! + * You only need to authenticate once, and that key lasts forever! + */ + extern QString SessionKey; + + enum Error + { + NoError = 1, // because last.fm error numbers start at 2 + + /** numbers follow those at http://last.fm/api/ */ + InvalidService = 2, + InvalidMethod, + AuthenticationFailed, + InvalidFormat, + InvalidParameters, + InvalidResourceSpecified, + OperationFailed, + InvalidSessionKey, + InvalidApiKey, + ServiceOffline, + SubscribersOnly, + + Reserved13, + Reserved14, + Reserved15, + + /** Last.fm sucks. + * There may be an error in networkError(), or this may just be some + * internal error completing your request. + * Advise the user to try again in a _few_minutes_. + * For some cases, you may want to try again yourself, at this point + * in the API you will have to. Eventually we will discourage this and + * do it for you, as we don't want to strain Last.fm's servers + */ + TryAgainLater = 16, + + Reserved17, + Reserved18, + Reserved19, + + NotEnoughContent = 20, + NotEnoughMembers, + NotEnoughFans, + NotEnoughNeighbours, + NoPeakRadio, + RadioNotFound, + APIKeySuspended, + + Reserved27, + Reserved28, + + RateLimitExceeded = 29, + + /** Last.fm fucked up, or something mangled the response on its way */ + MalformedResponse = 100, + + /** call QNetworkReply::error() as it's nothing to do with us */ + UnknownError + }; + + /** the map needs a method entry, as per http://last.fm/api */ + QNetworkReply* get( QMap<QString, QString> ); + /** generates api sig, includes api key, and posts, don't add the api + * key yourself as well--it'll break */ + QNetworkReply* post( QMap<QString, QString> ); + + + class ParseError : public std::runtime_error + { + Error e; + + public: + explicit ParseError(Error e); + ~ParseError() throw(); + + Error enumValue() const; + }; + + /** Generally you don't use this, eg. if you called Artist::getInfo(), + * use the Artist::getInfo( QNetworkReply* ) function to get the + * results, you have to pass a QDomDocument because QDomElements stop + * existing when the parent DomDocument is deleted. + * + * The QByteArray is basically reply->readAll(), so all this function + * does is sanity check the response and throw if it is bad. + * + * Thus if you don't care about errors just do: reply->readAll() + * + * Not caring about errors is often fine with Qt as you just get null + * strings and that instead, and you can handle those as you go. + * + * The QByteArray is an XML document. You can parse it with QDom or + * use our much more convenient lastfm::XmlQuery. + */ + QByteArray parse( QNetworkReply* reply ) throw( ParseError ); + + /** returns the expiry date of this HTTP response */ + QDateTime expires( QNetworkReply* ); + } +} + + +inline QDebug operator<<( QDebug d, QNetworkReply::NetworkError e ) +{ + return d << lastfm::qMetaEnumString<QNetworkReply>( e, "NetworkError" ); +} + +#define LASTFM_WS_HOSTNAME "ws.audioscrobbler.com" + +#endif @@ -44,6 +44,7 @@ #include <MDeclarativeCache> #include "src/hardwarekeyshandler.h" +#include "src/lastfmscrobbler.h" #include <QtSpotify> #include <qspotify_qmlplugin.h> @@ -52,6 +53,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) QApplication::setOrganizationName("MeeSpot"); QApplication::setOrganizationDomain("qt.nokia.com"); QApplication::setApplicationName("MeeSpot"); + QApplication::setApplicationVersion("1.1.1"); QSettings::setPath(QSettings::NativeFormat, QSettings::UserScope, QLatin1String("/home/user/MyDocs/.meespotconf")); @@ -64,6 +66,9 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) HardwareKeysHandler keyHandler; + LastFmScrobbler scrobbler; + view->rootContext()->setContextProperty(QLatin1String("lastfm"), &scrobbler); + view->setSource(QUrl("qrc:/qml/main.qml")); view->showFullScreen(); diff --git a/qml/LastfmLoginSheet.qml b/qml/LastfmLoginSheet.qml new file mode 100644 index 0000000..6194557 --- /dev/null +++ b/qml/LastfmLoginSheet.qml @@ -0,0 +1,152 @@ +/**************************************************************************** +** +** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Yoann Lopes (yoann.lopes@nokia.com) +** +** This file is part of the MeeSpot project. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** Redistributions of source code must retain the above copyright notice, +** this list of conditions and the following disclaimer. +** +** Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** +** Neither the name of Nokia Corporation and its Subsidiary(-ies) nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +** FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +** TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +****************************************************************************/ + + +import QtQuick 1.1 +import com.meego 1.0 +import "UIConstants.js" as UI + +MySheet { + id: lastfmSheet + + property alias title: label.text + + acceptButtonText: "Log in" + rejectButtonText: "Cancel" + platformStyle: SheetStyle { + headerBackground: "images/meegotouch-sheet-header-inverted-background.png" + } + + content: Column { + anchors.fill: parent + spacing: 20 + + Item { + width: 1; height: 1 + } + + Label { + id: label + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: UI.MARGIN_XLARGE + anchors.rightMargin: UI.MARGIN_XLARGE + font.pixelSize: UI.FONT_LARGE + } + + TextField { + id: username + placeholderText: "Username" + width: parent.width + platformStyle: TextFieldStyle { + backgroundSelected: "image://theme/" + appWindow.themeColor + "-meegotouch-textedit-background-selected" + } + platformSipAttributes: SipAttributes { + actionKeyLabel: (username.text.length > 0 && password.text.length > 0) ? "Log in" : "Next" + actionKeyEnabled: true + } + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText + Keys.onReturnPressed: { + if (username.text.length > 0 && password.text.length > 0) + accept(); + else + password.forceActiveFocus(); + } + onTextChanged: { + if (username.text.length > 0 && password.text.length > 0) + acceptButton.enabled = true; + else + acceptButton.enabled = false; + } + } + + TextField { + id: password + placeholderText: "Password" + echoMode: TextInput.Password + width: parent.width + platformStyle: TextFieldStyle { + backgroundSelected: "image://theme/" + appWindow.themeColor + "-meegotouch-textedit-background-selected" + } + platformSipAttributes: SipAttributes { + actionKeyLabel: (username.text.length > 0 && password.text.length > 0) ? "Log in" : "Next" + actionKeyEnabled: true + } + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText + Keys.onReturnPressed: { + if (username.text.length > 0 && password.text.length > 0) + accept(); + else + username.forceActiveFocus(); + } + onTextChanged: { + if (username.text.length > 0 && password.text.length > 0) + acceptButton.enabled = true; + else + acceptButton.enabled = false; + } + } + } + + onAccepted: { + authenticate(); + } + + function authenticate() + { + if (username.text.length > 0 && password.text.length > 0) + lastfm.authenticate(username.text, password.text); + } + + Timer { + id: timer + interval: 50 + onTriggered: username.forceActiveFocus(); + } + + onStatusChanged: { + if (status == DialogStatus.Opening) { + username.text = ""; + password.text = ""; + } else if (status == DialogStatus.Open) { + timer.start(); + } + } +} diff --git a/qml/MainPage.qml b/qml/MainPage.qml index 283db68..418d749 100644 --- a/qml/MainPage.qml +++ b/qml/MainPage.qml @@ -110,6 +110,14 @@ Page { } } + Connections { + target: lastfm + onErrorChanged: { + errorBanner.text = "Last.fm: " + lastfm.error; + errorBanner.show(); + } + } + TabGroup { id: tabGroup enabled: !currentTab.busy diff --git a/qml/PlaylistNameSheet.qml b/qml/PlaylistNameSheet.qml index 0a2d088..154ed4b 100644 --- a/qml/PlaylistNameSheet.qml +++ b/qml/PlaylistNameSheet.qml @@ -5,22 +5,22 @@ ** Contact: Yoann Lopes (yoann.lopes@nokia.com) ** ** This file is part of the MeeSpot project. -** +** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: -** +** ** Redistributions of source code must retain the above copyright notice, ** this list of conditions and the following disclaimer. -** +** ** Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. -** +** ** Neither the name of Nokia Corporation and its Subsidiary(-ies) nor the names of its ** contributors may be used to endorse or promote products derived from ** this software without specific prior written permission. -** +** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS @@ -87,5 +87,11 @@ MySheet { } } - onStatusChanged: if (status == DialogStatus.Opening) field.forceActiveFocus() + Timer { + id: timer + interval: 50 + onTriggered: field.forceActiveFocus(); + } + + onStatusChanged: if (status == DialogStatus.Open) timer.start() } diff --git a/qml/SettingsPage.qml b/qml/SettingsPage.qml index 8e930bd..e12f97e 100644 --- a/qml/SettingsPage.qml +++ b/qml/SettingsPage.qml @@ -5,22 +5,22 @@ ** Contact: Yoann Lopes (yoann.lopes@nokia.com) ** ** This file is part of the MeeSpot project. -** +** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: -** +** ** Redistributions of source code must retain the above copyright notice, ** this list of conditions and the following disclaimer. -** +** ** Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. -** +** ** Neither the name of Nokia Corporation and its Subsidiary(-ies) nor the names of its ** contributors may be used to endorse or promote products derived from ** this software without specific prior written permission. -** +** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS @@ -87,13 +87,20 @@ Page { } Switch { + id: offlineSwitch platformStyle: SwitchStyle { switchOn: "image://theme/" + appWindow.themeColor + "-meegotouch-switch-on-inverted" } anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - checked: spotifySession.offlineMode onCheckedChanged: spotifySession.setOfflineMode(checked) + + Component.onCompleted: checked = spotifySession.offlineMode; + + Connections { + target: spotifySession + onOfflineModeChanged: offlineSwitch.checked = spotifySession.offlineMode + } } } @@ -170,6 +177,97 @@ Page { Item { width: parent.width + height: UI.LIST_ITEM_HEIGHT + + LastfmLoginSheet { + id: lastfmLogin + title: "Scrobble to last.fm" + } + + Rectangle { + id: background + anchors.fill: parent + // Fill page porders + anchors.leftMargin: -UI.MARGIN_XLARGE + anchors.rightMargin: -UI.MARGIN_XLARGE + opacity: lastfmMouseArea.pressed ? 1.0 : 0.0 + color: "#22FFFFFF" + Behavior on opacity { NumberAnimation { duration: 50 } } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: lastfmSwitch.left + + Label { + id: titleText + width: parent.width + font.family: UI.FONT_FAMILY_BOLD + font.weight: Font.Bold + font.pixelSize: UI.LIST_TILE_SIZE + elide: Text.ElideRight + color: theme.inverted ? UI.LIST_TITLE_COLOR_INVERTED : UI.LIST_TITLE_COLOR + text: "Scrobble to last.fm" + } + Label { + id: selectedValue + width: parent.width + font.family: UI.FONT_FAMILY_LIGHT + font.pixelSize: UI.LIST_SUBTILE_SIZE + font.weight: Font.Light + elide: Text.ElideRight + color: theme.inverted ? UI.LIST_SUBTITLE_COLOR_INVERTED : UI.LIST_SUBTITLE_COLOR + text: lastfm.user + visible: text.length > 0 + } + } + + MouseArea { + id: lastfmMouseArea + anchors.fill: parent + onClicked: { + lastfmLogin.open() + } + } + + Switch { + id: lastfmSwitch + platformStyle: SwitchStyle { + switchOn: "image://theme/" + appWindow.themeColor + "-meegotouch-switch-on-inverted" + } + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + onCheckedChanged: { + if (checked && lastfm.user.length === 0) { + lastfmLogin.open(); + checked = false; + } else { + lastfm.enabled = checked + } + } + + Component.onCompleted: checked = lastfm.enabled; + + Connections { + target: lastfm + onEnabledChanged: lastfmSwitch.checked = lastfm.enabled + } + } + } + + Item { + width: parent.width + height: UI.MARGIN_XLARGE * 2 + + Separator { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + } + } + + Item { + width: parent.width height: UI.MARGIN_XLARGE } @@ -213,6 +311,7 @@ Page { onClicked: { spotifySession.logout(false); + lastfm.forgetUser(); } } @@ -66,5 +66,6 @@ <file>qml/MySelectionDialog.qml</file> <file>qml/MyCommonDialog.qml</file> <file>qml/Scrollbar.qml</file> + <file>qml/LastfmLoginSheet.qml</file> </qresource> </RCC> diff --git a/src/lastfmscrobbler.cpp b/src/lastfmscrobbler.cpp new file mode 100644 index 0000000..8c4118d --- /dev/null +++ b/src/lastfmscrobbler.cpp @@ -0,0 +1,237 @@ +/**************************************************************************** +** +** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Yoann Lopes (yoann.lopes@nokia.com) +** +** This file is part of the MeeSpot project. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** Redistributions of source code must retain the above copyright notice, +** this list of conditions and the following disclaimer. +** +** Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** +** Neither the name of Nokia Corporation and its Subsidiary(-ies) nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +** FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +** TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +****************************************************************************/ + +#include "lastfmscrobbler.h" + +#include <lastfm_key.h> +#include <ws.h> +#include <misc.h> +#include <XmlQuery.h> +#include <Track.h> +#include <qspotifysession.h> +#include <qspotifytrack.h> + +LastFmScrobbler::LastFmScrobbler() + : QObject() + , m_audioScrobbler(0) + , m_currentTrack(0) + , m_currentTrackCached(false) +{ + lastfm::ws::ApiKey = g_lastfmAPIKey; + lastfm::ws::SharedSecret = g_lastfmSecret; + + connect(QSpotifySession::instance(), SIGNAL(currentTrackChanged()), SLOT(onCurrentTrackChanged())); + connect(QSpotifySession::instance(), SIGNAL(currentTrackPositionChanged()), SLOT(onCurrentTrackPositionChanged())); + connect(QSpotifySession::instance(), SIGNAL(offlineModeChanged()), SLOT(onOfflineModeChanged())); + + restoreUser(); +} + +LastFmScrobbler::~LastFmScrobbler() +{ + delete m_audioScrobbler; + delete m_currentTrack; +} + +void LastFmScrobbler::authenticate(const QString &username, const QString &password) +{ + forgetUser(); + + QMap<QString, QString> params; + params["method"] = "auth.getMobileSession"; + params["username"] = username; + params["authToken"] = lastfm::md5((username.toUtf8() + lastfm::md5(password.toUtf8())).toUtf8()); + QNetworkReply* reply = lastfm::ws::post(params); + connect(reply, SIGNAL(finished()), this, SLOT(onAuthentificateResponse())); +} + +void LastFmScrobbler::onAuthentificateResponse() +{ + if (!sender()) + return; + + QNetworkReply *reply = dynamic_cast<QNetworkReply *>(sender()); + if (!reply) + return; + + try { + lastfm::XmlQuery const lfm = lastfm::ws::parse(reply); + lastfm::ws::Username = lfm["session"]["name"].text(); + lastfm::ws::SessionKey = lfm["session"]["key"].text(); + + delete m_audioScrobbler; + m_audioScrobbler = new lastfm::Audioscrobbler; + + m_user = lastfm::ws::Username; + m_sessionKey = lastfm::ws::SessionKey; + m_enabled = true; + saveUser(); + emit userChanged(); + emit enabledChanged(); + + } catch(lastfm::ws::ParseError e) { + forgetUser(); + + switch (e.enumValue()) { + case lastfm::ws::AuthenticationFailed: + m_errorMessage = "Invalid username/password."; + break; + case lastfm::ws::InvalidApiKey: + m_errorMessage = "Invalid API key."; + break; + case lastfm::ws::ServiceOffline: + m_errorMessage = "This service is temporarily offline. Try again later."; + break; + case lastfm::ws::TryAgainLater: + m_errorMessage = "There was a temporary error processing your request. Please try again."; + break; + case lastfm::ws::APIKeySuspended: + m_errorMessage = "Suspended API key. Please contact the developer."; + break; + default: + m_errorMessage = "Connection error."; + break; + } + + emit errorChanged(); + } +} + +void LastFmScrobbler::forgetUser() +{ + m_user = QString(); + m_sessionKey = QString(); + lastfm::ws::Username = m_user; + lastfm::ws::SessionKey = m_sessionKey; + m_enabled = false; + emit enabledChanged(); + emit userChanged(); + saveUser(); +} + +void LastFmScrobbler::setEnabled(bool e) +{ + if (m_enabled == e) + return; + + m_enabled = e; + emit enabledChanged(); + + saveUser(); + + if (!m_sessionKey.isEmpty() && m_enabled && !QSpotifySession::instance()->offlineMode()) { + m_audioScrobbler->submit(); + } else { + delete m_currentTrack; + m_currentTrack = 0; + m_currentTrackCached = false; + } +} + +void LastFmScrobbler::onCurrentTrackPositionChanged() +{ + if (!m_enabled || !m_currentTrack || m_currentTrackCached || lastfm::ws::SessionKey.isEmpty()) + return; + + uint played = QSpotifySession::instance()->currentTrackPlayedDuration() / 1000; + if (played >= m_currentTrack->duration() / 2 || played >= 240) { + m_audioScrobbler->cache(*m_currentTrack); + m_currentTrackCached = true; + } +} + +void LastFmScrobbler::onCurrentTrackChanged() +{ + if (lastfm::ws::SessionKey.isEmpty() || !m_enabled) + return; + + delete m_currentTrack; + m_currentTrack = 0; + m_currentTrackCached = false; + + QSpotifyTrack *track = QSpotifySession::instance()->currentTrack(); + if (!track) + return; + + m_currentTrack = new lastfm::MutableTrack; + m_currentTrack->setArtist(track->artists()); + m_currentTrack->setTitle(track->name()); + m_currentTrack->setTrackNumber(track->discIndex()); + m_currentTrack->setAlbum(track->album()); + m_currentTrack->setDuration(track->duration() / 1000); + m_currentTrack->setSource(lastfm::Track::Player); + m_currentTrack->stamp(); + + if (!QSpotifySession::instance()->offlineMode()) { + m_audioScrobbler->nowPlaying(*m_currentTrack); + m_audioScrobbler->submit(); + } +} + +void LastFmScrobbler::onOfflineModeChanged() +{ + if (!m_sessionKey.isEmpty() && m_enabled && !QSpotifySession::instance()->offlineMode()) + m_audioScrobbler->submit(); +} + +void LastFmScrobbler::saveUser() +{ + QSettings s; + s.setValue("lastfmUser", m_user); + s.setValue("lastfmSk", m_sessionKey); + s.setValue("lastfmEnabled", m_enabled); +} + +void LastFmScrobbler::restoreUser() +{ + QSettings s; + m_user = s.value("lastfmUser").toString(); + m_sessionKey = s.value("lastfmSk").toString(); + m_enabled = s.value("lastfmEnabled", false).toBool(); + lastfm::ws::Username = m_user; + lastfm::ws::SessionKey = m_sessionKey; + emit userChanged(); + emit enabledChanged(); + + m_audioScrobbler = new lastfm::Audioscrobbler; + + if (!m_sessionKey.isEmpty() && m_enabled && !QSpotifySession::instance()->offlineMode()) + m_audioScrobbler->submit(); +} diff --git a/src/lastfmscrobbler.h b/src/lastfmscrobbler.h new file mode 100644 index 0000000..9e59657 --- /dev/null +++ b/src/lastfmscrobbler.h @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Yoann Lopes (yoann.lopes@nokia.com) +** +** This file is part of the MeeSpot project. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** Redistributions of source code must retain the above copyright notice, +** this list of conditions and the following disclaimer. +** +** Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** +** Neither the name of Nokia Corporation and its Subsidiary(-ies) nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +** FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +** TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +****************************************************************************/ + +#ifndef LASTFMSCROBBLER_H +#define LASTFMSCROBBLER_H + +#include <QObject> +#include <Audioscrobbler.h> +#include <Track.h> + +class LastFmScrobbler : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString user READ user NOTIFY userChanged) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(QString error READ error NOTIFY errorChanged) +public: + LastFmScrobbler(); + ~LastFmScrobbler(); + + Q_INVOKABLE void authenticate(const QString &username, const QString &password); + + QString user() const { return m_user; } + Q_INVOKABLE void forgetUser(); + + bool enabled() const { return m_enabled; } + void setEnabled(bool e); + + QString error() const { return m_errorMessage; } + +Q_SIGNALS: + void userChanged(); + void enabledChanged(); + void errorChanged(); + +private Q_SLOTS: + void onAuthentificateResponse(); + void onCurrentTrackChanged(); + void onCurrentTrackPositionChanged(); + void onOfflineModeChanged(); + +private: + void saveUser(); + void restoreUser(); + + QString m_user; + QString m_sessionKey; + + bool m_enabled; + + lastfm::Audioscrobbler *m_audioScrobbler; + lastfm::MutableTrack *m_currentTrack; + bool m_currentTrackCached; + + QString m_errorMessage; + +}; + +#endif // LASTFMSCROBBLER_H |