summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYoann Lopes <yoann.lopes@nokia.com>2011-11-05 00:17:31 +0100
committerYoann Lopes <yoann.lopes@nokia.com>2011-11-05 00:17:31 +0100
commit1db565ca96009ceb7e1208823141e6f768afcd86 (patch)
tree3ffc5810d5249f8f229aeff829d6d93b9bfc1a3a
parent97e08a249e8324bf2525fc96961fcfd5212d8da6 (diff)
Added last.fm scrobbling.
-rw-r--r--.gitignore1
-rw-r--r--MeeSpot.pro13
-rw-r--r--README23
-rw-r--r--libQtSpotify/qspotifysession.cpp4
-rw-r--r--libQtSpotify/qspotifysession.h12
-rwxr-xr-xliblastfm/Album.h55
-rwxr-xr-xliblastfm/Artist.h62
-rwxr-xr-xliblastfm/Audioscrobbler.cpp142
-rwxr-xr-xliblastfm/Audioscrobbler.h107
-rwxr-xr-xliblastfm/NetworkAccessManager.cpp70
-rwxr-xr-xliblastfm/NetworkAccessManager.h58
-rwxr-xr-xliblastfm/NowPlaying.cpp69
-rwxr-xr-xliblastfm/NowPlaying.h41
-rwxr-xr-xliblastfm/Scrobble.cpp76
-rwxr-xr-xliblastfm/Scrobble.h63
-rwxr-xr-xliblastfm/ScrobbleCache.cpp136
-rwxr-xr-xliblastfm/ScrobbleCache.h74
-rwxr-xr-xliblastfm/ScrobblePoint.h59
-rwxr-xr-xliblastfm/ScrobblerHttp.cpp96
-rwxr-xr-xliblastfm/ScrobblerHttp.h74
-rwxr-xr-xliblastfm/ScrobblerSubmission.cpp69
-rwxr-xr-xliblastfm/ScrobblerSubmission.h48
-rwxr-xr-xliblastfm/Track.cpp147
-rwxr-xr-xliblastfm/Track.h219
-rwxr-xr-xliblastfm/XmlQuery.cpp64
-rwxr-xr-xliblastfm/XmlQuery.h77
-rwxr-xr-xliblastfm/global.h116
-rw-r--r--liblastfm/liblastfm.pri62
-rwxr-xr-xliblastfm/misc.cpp47
-rwxr-xr-xliblastfm/misc.h44
-rwxr-xr-xliblastfm/ws.cpp280
-rwxr-xr-xliblastfm/ws.h164
-rw-r--r--main.cpp5
-rw-r--r--qml/LastfmLoginSheet.qml152
-rw-r--r--qml/MainPage.qml8
-rw-r--r--qml/PlaylistNameSheet.qml18
-rw-r--r--qml/SettingsPage.qml111
-rw-r--r--res.qrc1
-rw-r--r--src/lastfmscrobbler.cpp237
-rw-r--r--src/lastfmscrobbler.h96
40 files changed, 3178 insertions, 22 deletions
diff --git a/.gitignore b/.gitignore
index e66e337..c5f4543 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
+
+
+
diff --git a/README b/README
index db84a65..b11f8eb 100644
--- a/README
+++ b/README
@@ -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
diff --git a/main.cpp b/main.cpp
index 1e86f81..006ba8d 100644
--- a/main.cpp
+++ b/main.cpp
@@ -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();
}
}
diff --git a/res.qrc b/res.qrc
index 61124f8..1bf0577 100644
--- a/res.qrc
+++ b/res.qrc
@@ -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